iOS原理分析(一)-对象大小与创建流程

207 阅读7分钟

最近开始了解iOS底层原理相关知识,好记性不如烂笔头,故此按自己理解记录下来以供回顾,若有理解不当之处,敬请指正!

源码和环境

objc-886.9 苹果官方开源代码
objc_debug objc可调试源码(推荐,也可以自己配置)
macOS 13.0.1
Xcode 14.1

对象创建流程图

对象创建流程.jpg 首先明确一点,对于[XXClass new][[XXClaas alloc] init],在不重写init的情况下,本质上并没有区别, [XXClass new]底层仍然是调用_objc_rootInit_objc_rootAllocWithZone,口说无凭,上源码

流程分析

[[XXClaas alloc] init]

alloc

入口 截屏2023-03-30 22.32.19.png

alloc

截屏2023-03-30 22.33.00.png

_obj_rootAlloc

截屏2023-03-30 22.34.26.png 这里将调用者class传入

callAlloc

截屏2023-03-30 22.35.34.png 这里需要区分allocWithZone非allocWithZone类型.allocWithZone类型直接通过objc_msgSend调用allocWithZone以初始化;非allocWithZone则调用_objc_rootAllocWithZone.这里根据hasCustomAWZ决定走向,其内部实现为:

截屏2023-03-30 22.58.01.png

截屏2023-03-30 22.58.33.png 根据其注释得知,决定因素为当前类或其父类是否实现allocallocWithZone:。该观点将在后续论证环节论证。

_objc_rootAllocWithZone

截屏2023-03-30 23.28.32.png 可以看到,这里调用了_class_createInstancesFromZone且忽略了zone参数

_class_createInstancesFromZone

截屏2023-03-31 00.02.59.png 终于到了对象创建的核心部分,除了开头的一些异常处理,指定Isa外,核心方法为instanceSize获取对象大小以及calloc创建对象。需要留意的是,这里对instanceSize返回的对象大小设置了最小值16,也就是说,一个对象最小内存大小为16Byte,这里后续会用例子论证。

instanceSize

截屏2023-03-31 00.13.49.png 内部核心为调用fastInstanceSize

fastInstanceSize

截屏2023-03-31 00.24.41.png 这里从内存得到size,再调用align16处理

align16

截屏2023-03-31 00.27.05.png _class_createInstancesFromZone在拿到size后调用calloc开辟对象空间,这里会做16字节对齐,即size为16的整数倍,如 16 32 48...

init

init

截屏2023-03-30 23.37.58.png

_objc_rootInit

截屏2023-03-30 23.42.54.png 从代码和内存地址可知,init仅是返回了当前对象,这个主要提供给工厂模式下重写该方法以实现自定义的初始化。至此便完成了[[XXClass alloc] init]对象的创建。再看一下[XXClass new]方式

[XXClaas new]

截屏2023-03-30 23.47.24.png 可以看到,其内部也是调了[callAlloc init],allAlloc init接上上文中的callAlloc init 部分,这里不再赘述

论证

1. callAlloc区别

我们通过对测试类allocWithZone实现与否进行对照
添加前: 截屏2023-03-30 23.02.58.png

截屏2023-03-30 23.06.37.png

添加后: 截屏2023-03-30 23.02.37.png 截屏2023-03-30 23.09.55.png 可以看到,在实现allocWithZone:前调用_objc_rootAllocWithZone,实现后则通过objc_msgSend调用实现的alloc/allocWithZone方法,这里由于实现的alloc/allocWithZone内部仍调用[Student new],将会陷入循环,仅做示例,实际开发不可取。 截屏2023-03-30 23.25.56.png

2. 内存重排

2.1 基础数据类型

我们在Student类中添加属性name(NSStirng),age(int),height(double),值得留意的是iOS类中默认有isa指针,大小为8字节
截屏2023-04-01 18.08.57.png

main函数创建对象并赋值 并调用malloc_size获取并打印对象大小 截屏2023-04-01 20.42.09.png 运行后得到cici对象大小为32,根据我们Student内属性的计算,总和应该是28,这里就涉及到内存对齐的概念,简单来说,对象大小必须是8的倍数,如果实际大小不够,则向上对8取整所以cici对象size为32。

通过调试工具打印对象地址,并逐个打印数值,相关指令请查看《iOS分析指令汇总》 截屏2023-04-01 21.28.19.png 因为对象首8w位为isa指针,将在后续文章中详细说明,这个先从对象首地址偏移8位来打印,在这个例子中即0x0000000000000012。根据打印可以看到0x0000000000000012对应为age,0x0000000100004040对应name,0x4067200000000000对应height,其在内存的中的顺序与对象的定义,赋值均无联系内存结构如下图所示 页-1.jpg

我们再在Student对象中加入一个int类型字段,重复上文的赋值打印对象大小操作 截屏2023-04-01 21.50.19.png 截屏2023-04-01 21.51.30.png 这里与我们预想中的值不符,按上文中说的8字节对齐,这里cici对象大小应该是40,别急,我们打印一下对象内存

截屏2023-04-01 21.55.52.png 与上文中的对象内存对比发现,首地址偏移8位后的内存地址发生了改变,尝试将该内存分成 4+4两个地址进行打印,得到的正是对应的ageno字段,这就是iOS的内存重排:对于类中的基础数据类型成员变量,在8字节可容纳的前提下,会把不相应的数据放在同一个8字节内存中,以节省内存
内存示意图如下,需要注意的是,对于int等基础数据类型,在内存中并不是以对象的形式存在,而是以指针即数值的形式,指针地址就是对应的数值,我们将0x000000120x00000065转十进制即为 18 和 101。相关内容将在后续的iOS内存管理相关文章的tagged pointer部分叙述。 页-1(1).jpg

2.2 类继承

再看一下类对象对对象内存的影响,创建Person类作为Student类的父类,在PersonStudent添加成员变量如图

截屏2023-04-03 21.10.26.png 截屏2023-04-03 21.10.39.png 通过malloc_sizeclass_getInstanceSize分别打印分配的内存大小和对象的实际大小 截屏2023-04-03 21.12.08.png 可以看到,分配内存和实际大小都是32,Person中 8+8+4=20, Student中 8(isa)+4=12,所以Student对象大小32正好,Student成员变量agePerson成员变量count在一个8字节内存段,证明在这种情况下系统会对子类父类的成员变量进行重排。

在上面代码基础上,调整Person类成员变量顺序如下,再看一下结果

截屏2023-04-03 21.14.39.png 截屏2023-04-03 21.16.15.png 可以看到,由于我们变更了父类 age成员变量的声明顺序,得到的对象的系统分配内存大小和实际大小都与调整前不同且均有所增加。在通过打印内存地址内容发现,同样4字节的两个字段并没有进行合并重排。由此得出,父类子类成员变量重排的条件是父类可重排对象在最后声明,且子类可重排成员变量在最前声明。因为子类生成对象时,是将父类成员变量作为一个整体插入子类中,子类无法将子类对象插入到父类前面,只能从父类后面追加子类成员变量。

根据第一种情况再调整代码,我们将Student类成员变量调整为属性,再看一下效果

截屏2023-04-03 21.22.38.png 截屏2023-04-03 21.27.09.png 可以看到,在子类改为属性之后,依然进行了内存重排。由此可以得出结论,在子类和父类满足重排条件和顺序的情况下,子类是属性的形式还是成员变量的形式,不影响最终的重排效果。

为了验证子类属性和成员变量的声明顺序对内存重排的影响,我们分别增加子类的成员变量和属性并调整声明顺序看一下
添加属性:
截屏2023-04-03 22.00.35.png 调整属性顺序:
截屏2023-04-03 22.07.29.png 添加成员变量:
截屏2023-04-03 22.10.11.png 调整成员变量顺序:
截屏2023-04-03 22.12.41.png 可以看到,子类是成员变量还是属性以及成员变量和属性的顺序,均不影响内存重排和最终对象的大小

根据第一种情况再调整代码,我们将Person类成员变量调整为属性,再看一下效果

截屏2023-04-03 21.31.24.png 截屏2023-04-03 21.32.17.png

可以看到,在父类改为属性之后,内存重排并没有发生。由此说明了父类是属性还是成员变量,是会决定是否进行内存重排并影响最终的对象大小的

总结

1.对象最小为16字节
2.allocnew方法初始化对象本质上是一样的,只是alloc方式可以自定义后续的init方法,以满足工厂模式需求
3.内存8字节对齐,不满部分若满足条件会进行重排,不满足重排条件则保留空间。
4.父类的成员变量声明顺序影响内存重排,子类不影响