美文网首页
OC对象的底层探索(上)

OC对象的底层探索(上)

作者: verylinming | 来源:发表于2022-05-04 20:06 被阅读0次
01-alloc方法在底层的调用流程

1、在创建OC对象的时候,常用的方式就是alloc init方法,但是这两个方法底层究竟做了什么呢?

    1. 首先新建一个Person类,打印以下三个变量的值
  Person *p = [Person alloc];
  Person *p1 = [p init];
  Person *p2 = [p init];      
  
  NSLog(@"%@", p);
  NSLog(@"%@", p1);
  NSLog(@"%@", p2);

从结果中可以看到,三个变量是一样的。

截屏2022-04-18 下午6.50.05.png
    1. 接下来,打开汇编窥探一下底层做了什么。

Xcode打开汇编:Debug -> Debug Workflow -> Always Show Disassembly

Person *p = [Person alloc];前打一个断点,运行程序,可以看到,在调用alloc时底层实际上调用了objc_alloc

截屏2022-04-18 下午6.48.55.png

接下来打一个全局objc_alloc符号断点,然后跳到objc_alloc方法内部。

截屏2022-04-18 下午6.56.24.png 截屏2022-04-18 下午6.59.17.png

一步一步断点调试,最终会执行objc_msgSend方法。此时分别打印rdi和rsi寄存器,可以看到objc_msgSend实际上就是Person类在调用alloc方法(po打印不出地址内容时,可以将地址强转为char *类型)。

截屏2022-04-18 下午7.12.26.png

此时为alloc方法下一个符号断点,查看alloc方法对应的汇编代码,可以看到alloc方法底层调用了_objc_rootAlloc方法。

截屏2022-04-18 下午7.18.45.png

点击下一步断点,进入方法内部,一步一步调试,可以看到最终调用了_objc_rootAllocWithZone方法。

截屏2022-04-18 下午7.21.49.png

截止目前,alloc方法的底层调用流程为:alloc -> objc_alloc -> objc_msgSend -> alloc -> _objc_rootAlloc -> _objc_rootAllocWithZone

    1. 接下来通过汇编就不能继续往下探索了,我们可以通过objc源码来继续探索

打开苹果官方开源代码链接,下载最新objc4-838.1文件。

然后同样新建一个Person类,并在main方法中初始化。

进入alloc方法,分别在_objc_rootAllocobjc_alloccallAlloc_objc_rootAllocWithZone等处打上断点。

运行程序,会发现第一次进入断点就走到了objc_alloc方法中,在objc_alloc中又调用了callAlloc方法。

截屏2022-04-18 下午7.43.20.png

callAlloc方法中最终又调用了alloc方法。

截屏2022-04-18 下午7.40.14.png

alloc方法中,又来到了_objc_rootAlloc方法。

截屏2022-05-04 15.03.42.png

_objc_rootAlloc方法中最终又调用了callAlloc方法,这次在callAlloc中最终调用了_objc_rootAllocWithZone方法。

截屏2022-05-04 15.06.15.png

_objc_rootAllocWithZone方法内部又调用了_class_creatInstanceFromZone方法。

截屏2022-05-04 15.02.25.png

自此,可以得到alloc方法的完整底层调用流程:alloc -> objc_alloc -> callAlloc -> objc_msgSend -> alloc -> _objc_rootAlloc -> callAlloc -> objc_rootAllocWithZone -> _class_creatInstanceFromZone

    1. 在第一次调用alloc时没有走_objc_rootAlloc,是因为系统内部有个fixupMessageRef方法,修改了alloc方法的指向:当sel==alloc时,imp=objc_alloc。
截屏2022-05-04 15.08.03.png
    1. 查看汇编,在_objc_rootAllocWithZone方法中的ret处打一个断点,打印返回值,可以看到最终在调用_objc_rootAllocWithZone时返回了对象。
截屏2022-05-04 15.09.15.png
    1. 而init方法底层是直接返回。苹果这样设计init方法的目的是工厂模式、方便重写。
截屏2022-05-04 15.31.13.png
02-关于编译器的优化

1、在汇编代码中看不到callAlloc方法和_class_creatInstanceFromZone方法的调用,是编译器优化的结果:Build Settings -> Apple Clang - Code Generation -> Optimization Level。注意选择None不代表不优化,只是优化等级没那么高。

03-对象的内存对齐方式

1、第一节说到,alloc方法最终是在classcreateInstanceFromZone中创建的对象。查看classcreateInstanceFromZone方法的源码,其中instanceSize是计算对象需要的大小(可以看到最小16个字节),alignedInstanceSize是内存对齐算法(iOS 64位下8字节对齐),word_Align是8字节对齐算法。

截屏2022-05-04 16.33.39.png 截屏2022-05-04 16.33.58.png 截屏2022-05-04 17.49.41.png
    1. 只要是8的倍数,低三位都是0。(x + 7) & ~7:低三位清零,8字节对齐。32位时,(x + 3) & ~3:低两位清零,4字节对齐。先右移再左移也可以实现低位清零。
    1. instanceSize在有缓存时是16字节对齐,无缓存时是8字节对齐。
截屏2022-05-04 18.01.56.png 截屏2022-05-04 18.01.15.png

2、instanceSize方法只是计算需要的内存空间大小,实际申请内存空间是通过calloc方法,查看calloc方法实现需要打开另外一份源码libmalloc。

    1. calloc内部是以16字节对齐的,而对象以8字节对齐,这样做的目的是以空间换时间,内存读取是以内存块为单位的。isa就是8个字节,对象至少占用8个字节,如果也以8个字节对齐,挨在一起计算较多,容易混乱,以16字节读取效率更高。
截屏2022-05-04 18.16.39.png
04-对象的本质

1、对象的本质是什么?

    1. clang -rewrite-objc main.m,编译main.m文件,得到main.cpp文件;
    1. 可以看到,对象编译完最终是一个objc_object的结构体,对象(objc_object)里存储的内容是isa指针 + 成员变量的值。
05-结构体的内存对齐方式

1、结构体内存对齐规则总结

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

2、结构体内部成员变量的顺序不同时,结构体大小也会受影响。

3、结构体中的结构体不作为一个整体参与计算最大成员,但结构体中的结构体的成员变量参与。

---- 此文章内容仅为个人学习总结,仅供参考。

相关文章

网友评论

      本文标题:OC对象的底层探索(上)

      本文链接:https://www.haomeiwen.com/subject/akulertx.html