iOS对象的底层探索(上)

257 阅读5分钟

平时在创建对象的时候都是直接调用[[xxx all] init]alloc init都发生了什么了?首先看下面一段代码

    LSPerson *person = [LSPerson alloc];

    LSPerson *person1 = [person init];

    LSPerson *person2 = [person init];

    // 二者输出的地址一样,等同于 person1 = person; person2 = person
    NSLog(@"person1: %@, person2: %@", person1, person2);

通过上面我们发现,alloc方法里面已经创建了对象(person1、person2的地址同person),init并没有对对象做出改变。那么调用alloc到底发生了什么了?底层方法是如何实现的了?

一、alloc方法的底层调用流程

1、通过objc源码分析alloc的底层调用流程

通过源码,我们在每个方法增加断点,具体的代码如下所示

    [LSPerson alloc]
    [NSObject alloc]
    [NSObject _objc_rootAlloc]
    [NSObject callAlloc]
    [objc-runtime _objc_rootAllocWithZone]
    [objc-runtime _class_createInstanceFromZone]

image.png

通过对上面的方法进行断点发现并没有按照 [NSObject alloc] -> [NSObject _objc_rootAlloc] -> [NSObject callAlloc] 这种调用流程。而是先调用了 [NSObject callAlloc] 然后再执行上面的流程,也就是callAlloc执行了两遍。那为什么会这样了?

2.汇编分析执行流程,为什么会执行两遍callAlloc

汇编断点的位置:Debug -> Deubg Workflow -> Always show Disassembly

image.png

此时我们发现,调用alloc,首先会先调用objc_alloc,结合源码我们会发现 objc_alloc 会调用callAlloc方法,为什么会先调用objc_alloc? 是因为系统会在 fixupMessageRef 方法中修改 sel = alloc 的imp。

image.png

继续加入符号断点objc_all, 发现会进入objc_messageSend 函数(这个地方也可以结合objc源码查看),通过寄存器指令查看,此时objc_messageSend 函数的sel 为 LSPerson, sel 为 alloc 方法。 image.png

此时再增加符号断点[NSObject alloc], 会继续调用_objc_rootAlloc(源码中会继续调用callAlloc方法) image.png

点击进入_objc_rootAlloc 方法,发现会调用_objc_rootAllocWithZone, 通过对objc源码的分析,_objc_rootAllocWithZone 会返回id,在源码中发现最后调用的是_class_createInstanceFromZone,该方法返回的是一个obj?那么这个obj是否是我们的创建的对象了? image.png

我们知道ret在汇编中是返回的指令,我们retab增加断点,点击之后,通过读取寄存器x0地址,发现返回的是LSPerson对象 image.png

alloc方法调用总结

image.png

  • callAlloc方法执行了两遍,是因为系统在fixupMessageRef函数里面修改了alloc方法的imp,会先执行objc_alloc方法(具体执行流程入上图所示)
  • alloc 方法已经创建了对象并且开辟了内存

三、编译器优化

我们在汇编执行的时候发现很多方法没有在汇编中显示出来,比如_class_createInstanceFromZone,这是为什么了? 其实是编译器做了优化,编译器的优化可以在 target->Build->Optimization Level下修改

image.png 默认在debug模式下是None(None并不是不优化)

四、init方法

image.png

通过对上面的分析,我们发现在alloc的时候对象已经创建了,并且调用init 返回的也是self/obj,那么为什么要这么设计了?这里主要是采用了工厂模式让开发者可以重写init方法,可以在init方法里面做一些初始化操作。

五、alloc内存申请

上面我们提到_class_createInstanceFromZone方法会开辟内存返回对象,那么开辟内存是根据什么规则了?我们通过objc源码查看该方法实现

image.png image.png 通过上面的代码我们知道会首先调用instacSize,在内部有一个判断if (size<16) size=16;,也就是说通过alloc创建出来的对象的最小的也是16个字节,我们再看一下内存大小的计算方法

没缓存的时候计算方法: image.png image.png 有缓存的时候的计算方法: image.png image.png

从上面我们可以看出,系统创建对象开辟内存是以8个字节来进行对齐的(64位机器),如果有缓存的时候则是以16个字节对齐,那么这个计算出的大小是实际对象的内存大小吗?通过源码继续往下查看 image.png 计算好对象需要的内存size之后,真正开辟内存是调用的calloc函数,calloc函数内部是以16字节对齐的(需要去源码里面查看)。并且从上面代码obj->initInstanceIsa方法中,把calloc返回的内存地址关联到我们这个类。至此我们也可以得出_class_createInstanceFromZone方法内部主要做了以下几个操作:

_class_createInstanceFromZone作用:
1、cls->instanceSize: 先计算出需要的内存空间大小
2、calloc: 向系统申请开辟内存,系统会做16位对齐,然后返回地址指针
3、obj->initInstanceIsa: 关联到相应的类
4、return obj

六、对象的本质

我们在第5部分讲,字节对齐计算用8字节对齐,申请开辟内存的时候实际已16个字节来对齐,那为什么要这样了?首先我们来先看看对象的本质?

新建一个demo,写一个LSPerson类,在终端里面进入当前项目然后执行clang -rewrite-objc main.m -o main.cpp命令(将main.m 编译成main.cpp文件),打开main.cpp文件搜索LSPerson,如下图所示: image.png 此时发现对象的本质是一个objc_object的结构体其结构内有一个isa的指针isa指针占用8个字节,那么对象至少需要8个字节,如果以8个字节对齐的话,如果连续的两块内存都是没有属性的对象,那么他的内存空间就会完全的挨在一起,是容易混乱的。以16字节为一块,这就保证了CPU在读取的时候,按照块读取就可以,效率更高,同时还不容易混乱。

那为什么要用块了? CPU在读取内存的时候是以块为单位来读取的,块的大小也就是内存存取的力度。如果不对齐的话,在我们频繁的的存取内存的时候,CPU就需要花费大量的精力去分辨你要读取多少字节,这就会造成CPU的效率低下,如果像高效读取数据,这个规范就是字节对齐。

七、结构体对齐方式

在上面我知道了对象的本质是结构体, 对象的内存对齐方式我们知道了,那么结构体的内存对齐是什么规则了?

image.png 从上面的代码我们可以得出几个结论:

1. 结构体的大小跟结构体成员类型有关 2. 结构体的大小跟结构体成员顺序有关

引用一份著名结构题对齐规则的截图: image.png 我们按照上面的截图规则再去验证一下: image.png

按照上面规则验证跟输出结果一致,接下来再看看,结构体里面包含结构体成员变量: image.png

总结

本文章主要讨论了alloc的执行过程,以及alloc的内部方法调用分析,alloc开辟内存的分析,以及最后的对象的本质,从而知道了结构体,然后又分析了结构体内存对齐规则。