iOS底层原理之对象的底层探索(上)

2,195 阅读6分钟

本文主要内容

对象的底层探索
1.alloc底层调用流程探究
2.编译器的优化
3.对象的内存对齐方式
4.对象的本质
5.结构体的内存对齐方式

一、背景

开发过程中,我们创建某个类的实例对象时通常会使用alloc、init,例如创建一个HGPerson类型的对象p,则会写:

HGPerson *p = [[HGPerson alloc] init];

为了研究对象到底是如何创建的,将其分开为如下写法并打印:

HGPerson *p = [HGPerson alloc];
HGPerson *p1 = [p init];
HGPerson *p2 = [p init];
NSLog(@"p = %@, p1 = %@, p2 = %@",p, p1, p2);

打印结果为:

p = <HGPerson: 0x6000034d80d0>, p1 = <HGPerson: 0x6000034d80d0>, p2 = <HGPerson: 0x6000034d80d0>

由此发现对象p、p1、p2的内存地址完全一致,即代表是同一个对象。为了进一步验证其正确性,为HGPerson类添加一个属性:

@property (nonatomic, assign, readwrite) int age;

给对象p的age属性赋值并添加打印:

p.age = 18
NSLog(@"p1.age = %d, p2.age = %d",p1.age, p2.age);

打印结果为:

p1.age = 18, p2.age = 18

进一步说明对象p、p1、p2为同一个对象!实在太神奇了! 同时得出一个结论:

在使用alloc、init创建对象时,只有alloc会开辟内存空间,而init不会!

想搞清楚alloc时苹果在底层到底做了哪些事情,需要去研究其底层源码,找到对应的源码,本文使用“objc4-838.1”。

Apple open source官方获取地址:

源码原地址:opensource.apple.com/tarballs/(已停更)
源码最新地址:opensource.apple.com/releases/

二、探究底层的三种方法

(1)在"HGPerson *p = [HGPerson alloc];"处打断点,模拟器和真机分别运行,到断点处后,按住Control+Step into,找到底层函数objc_alloc,此时无法看到底层的源码。

模拟器运行:

11652681220_.pic.jpg

31652681465_.pic.jpg

为了研究底层,继续Control+Step into,即可找到"libobjc.A.dylib`objc_alloc"(添加Symbolic Breakpoint符号断点(objc_alloc)),结合源码继续分析.

image.png

真机运行:

图片1.png

图片2.png

(2)通过汇编跟流程,先运行工程等待跳转到"HGPerson *p = [HGPerson alloc];"断点处,在Debug -> Debug Workflow -> Always show Disassembly打开汇编调试工具,找到objc_alloc,添加符号断点跟踪。

image.png

模拟器运行:

image.png

真机运行:

image.png

(3)通过已知方法即alloc,添加符号断点alloc,找到“libobjc.A.dylib`+[NSObject alloc]”:

image.png

image.png

image.png

三、alloc底层调用流程探究(前)(汇编+符号断点+源码)

以真机为例
第一步,在"HGPerson *p = [HGPerson alloc];"处打断点,运行到断点后,打开汇编调试,立刻跳转到汇编调试界面,如下:

image.png

跳转到符号objc_alloc,对应上层即为调用“objc_alloc”函数,添加符号断点“objc_alloc”,点击“Continue program execution”继续运行:

image.png

可以看到此时汇编中有2个"b“跳转指令,单步运行查看实际执行哪个跳转,发现执行第2个跳转,如下图:

image.png

读取寄存器发现,第2个跳转“objc_msgSend”函数实际就是通过“HGPerson”调用了"alloc"方法。
第二步,添加"[NSObject alloc]"符号断点:

image.png

第三步,Control+Step into,进入_objc_rootAlloc内部,又存在两个跳转b,单步执行后实际调用_objc_rootAllocWithZone

image.png

第四步,再进入 _objc_rootAllocWithZone内部,有ret即reture指令,结合源码和寄存器读取得出结论:在_objc_rootAllocWithZone函数后即返回一个对象!也就是说如果类继承自NSObject时,创建类对象时直接调用alloc方法即可创建一个对象

image.png

总结alloc的底层流程图如下:

image.png

补充内容探究init的作用

HGPerson *p1 = [p init];处打断点并运行工程,执行到此处时添加符号断点[NSObject init],其内部只有一个ret返回,读取此时的寄存器,返回HGPerson对象。

image.png

init的作用:
一种工厂模式。供开发者重写init方法,在内部进行初始化赋值等操作,如使用NSArray等时不调用init方法会出现错误。

四、编译器优化

在“三、alloc底层调用流程探究”的第四步,汇编跟踪到_objc_rootAll- -ocWithZone内部即显示ret返回,而在源码_objc_rootAllocWithZo- -ne函数中调用_class_createInstanceFromZone函数才会返回对象。为什么会出现这种情况呢?

image.png

实际上是编译器进行了优化处理。在工程的"Build Settings"中的“Optimization Level”即可设置编译器优化等级。

image.png

以下简单举例说明:
默认编译器优化等级Debug为“None” image.png

image.png

打断点后汇编调试,即使没有使用a、b,仍然会显示出来。

image.png

image.png

修改等级为“Fastest[-O3]”,没有引用的变量会被优化。

image.png

image.png

五、alloc底层调用流程探究(后)(汇编+符号断点+源码)

结合以上编译器优化的性能,在源码中再次进行alloc调用流程跟踪: alloc -> objc_alloc -> callAlloc -> objc_msgSend -> alloc -> _objc_rootAlloc -> callAlloc -> _objc_rootAllocWithZone -> _class_createInstanceFromZone.

注意⚠️:在汇编跟踪时,2次callAlloc未显示,是因为编译器的优化导致。

image.png

image.png

image.png

image.png

image.png

image.png

image.png

备用知识栏
汇编指令和LLDB相关命令见如下文章: juejin.cn/post/709823…

六、对象的内存对齐(字节对齐算法)

在创建实例对象的_class_createInstanceFromZone函数中,对象内部内存是以8字节对齐,而系统分配内存时以16字节对齐,以空间换时间,CPU读取内存时以内存块为单位读取,确保CPU的高效读取。
分析对象内存大小计算过程:
1.调用函数instanceSize->alignedInstanceSize->word_align ,calloc函数结合libmalloc源码看底层。

image.png

image.png

注意:
1.可以看出“instanceSize“只是计算对象需要的内存大小,对象的内存大小最终由“obj = (id)calloc(1, size);”决定,其内部是以16字节对齐的;
2.“alignedInstanceSize”中可以看出内存大小最小为16字节。

image.png

image.png

2.内存对齐算法,64位时8字节对齐,32位时4字节对齐。

image.png

8字节对齐算法解读:return (x + WORD_MASK) & ~WORD_MASK; 
即(x + 7) & -7,相当于(x + 7) >>3 <<3,低3位清0。
16字节对齐算法解读:return (x + size_t(15)) & ~size_t(15); 
相当于(x + 15) >>4 <<4,低4位清0。

总结:由于对象中一定含有isa指针,其占用8字节,为了避免因不同对象内存紧挨可能导致的误读,同时提高CPU的读取速率,采用以空间换时间的方式,遵循16字节对齐(综合考虑,最优效果,如果更高如32字节浪费内存),提高CPU效率。

七、对象的本质

创建一个空工程(02_对象的本质),main.m路径下,在终端使用clang命令编译main.m文件,clang -rewrite-objc main.m,得到main.cpp文件。

image.png

main.cpp文件中搜索HGPerson,可以看到HGPerson本质是一个objc_object类型的结构体,结构体中包含isa + 成员变量的值(age和name).

image.png

扩展:实际上,isa也是一个objc_class类型的结构体!

image.png

image.png

八、结构体的内存对齐

1.结构体内存对齐的规则

  • 数据成员对齐规则:结构体(struct)的第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小的整数倍开始(比如int为4字节,则要从4的整数倍地址开始存储);
  • 结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从内部最大元素大小的整数倍地址开始存储。(struct a里存有struct b,b里有char,int,double等元素,那b应该从8的整数倍开始存储);
  • 收尾工作:结构体的总大小,也就是sizeof的结果必须是其内部最大成员的整数倍,不足的要补齐。

如下图举例详细说明

image.png

特别注意:
1.struct4结构体中直接最大元素为int类型,如果最终的sizeof大小为4的倍数即为36,但实际为40,即说明,如果结构体内部包含结构体,则结构体内部最大成员包含其内部结构体的最大成员!
2.结构体的内存大小和其内部元素的排列顺序有关,顺序不同,分配的内存不同!

思路总结:底层探索流程

  1. Apple底层的内容太多,如何开始;
  2. 拿出对象最熟悉的alloc作为案例开始研究分析;
  3. 如何分析:通过底层源码了解;
  4. 底层源码分析方法:3种方法;
  5. 通过底层源码借助汇编等相关调试方法开始流程分析;
  6. 通过调试得出alloc的流程分析图,了解编译器的优化;
  7. 根据文章开头发现alloc时就是开辟内存,开始分析内存;
  8. 通过LLDB调试验证对象的内存,得出对象的内存对齐方式、结构体的内存对齐方式等。

有任何问题,欢迎👏各位评论指出!觉得博主写的还不错的麻烦点个赞喽👍