OC对象alloc流程探索

675 阅读6分钟

准备工作

苹果开源地址:opensource.apple.com/tarballs

1、下载最新的OC源码,本文编写时最新代码为objc4-824,但下载时网站会报错,所以这里使用的是上一个版本objc4-818.2版本,这个源码工程编译会报很多错误,需要做很多的系统文件补充、配置修改等工作,所以我们可以到github上下载修改好的可编译源码 github可编译源码地址

2、下载llvm源码,可以在苹果开源网址下载,也可以github搜索llvm project下载,下载好的源码可以编译成Xcode工程,然后用xcode编译运行,按照github上的readme文档编译即可。这里我们暂时不用编译。

3、下载libmalloc源码,同样编译会报错,如果不想自己修改也可以到github上搜索别人修改好的可编译的版本

对象创建过程

我们要探索OC对象的原理,首先肯定要从创建对象开始,新建一个工程,自定义一个类Person继承自NSObject,然后我们在生成的ViewController的viewDidLoad方法内初始化这个类。

Person *p1 = [Person alloc]; 当前p1对象由alloc方法初始化,那么我们按住ctl+command+鼠标左键点进alloc方法内部

image.png

继续进入alloc代码,坏了,点不进去了,确实看不到内部实现了,这是因为还没有导入源码,我们进入苹果开源网站看一下

image.png

这么多源码究竟下载哪个呢,这时我们回到Xcode,给alloc方法添加一个符号断点,点击失效。然后在Person对象创建的位置打个断点,运行代码,断到对象创建位置,然后在激活符号断点,这样做可以防止其他对象的初始化干扰我们对Person对象alloc的判断,继续运行可以看到符号断点位置如下

image.png

可以看到NSObject的alloc方法在libobjc.A.dylib库中,这个库就是我们的objc4源码,那我们打开从github下载的修改好的可编译源码工程,搜索alloc,哗的一下出来一大堆,找不到哪个是alloc的实现,这里有个小技巧,我们可以搜索 alloc { 这里中间有个空格,这样可以快速搜索到方法实现。我们可以看到alloc内部return了 _objc_rootAlloc(self) 方法,继续点击进入,内部return了 callAlloc(cls, false/checkNil/, true/allocWithZone/) 方法,继续点击进入,我们可以看到 callAlloc 内部实现如下

image.png

这里我们看到有多个判断,首先我们使用的是2.0版本的OC可定会进入 #if 的内部,内部有两个判断,要么什么都不执行,要么执行 _objc_rootAllocWithZone(cls, nil) ,要么执行 objc_msgSend 。那么具体如何执行呢,我们在源码工程内创建一个target,在target内创建main.m文件和main方法,在main方法内初始化Person对象,在Person对象创建处断点,运行代码,程序中断,然后我们在对应的位置分别打上断点,继续执行程序,我们会发现程序执行的是 objc_msgSend 向Person的alloc发送了消息,这就很奇怪了,我们创建对象执行的是alloc方法,经过一系列方法中转后为什么又重新执行一次alloc呢,这样看上去好像什么都没做啊,我们带着这个问题继续执行程序,发现这次执行的alloc会最终执行 _objc_rootAllocWithZone(cls, nil) ,程序肯定不会无缘无故重复执行一次alloc,我们开启汇编重新跑一次,我们发现在进入我们看到的源码内部执行之前先调用了 objc_alloc 方法 。

image.png

说明OC在alloc的SEL和IMP绑定之前做了处理,那么我们全局搜索一下objc_alloc方法看一下,最终找到如下代码

image.png

代码的意思就是OC做的message相关的修复过程,可以看到,当执行到SEL为alloc的时候IMP被替换成了objc_alloc,那么我们在跳转到objc_alloc的实现发现return的是callAlloc方法,整个过程看似好像什么都没有做,只是添加了一步objc_alloc,按照苹果的尿性不可能什么都不做,那么我们就再向前一步到编译阶段看看。 用VSCode打开llvm project,搜索objc_alloc,经过一番查找,发现这样的代码

image.png

进入EmitObjcAlloc看到如下代码

image.png

我们看到这里有alloc到objc_alloc的处理,继续进入emitObjCValueOperation,看到如下代码

image.png

这里我们看到alloc被替换为objc_alloc并执行,和标记已执行的动作。保证下次通过objc_msgSend调用alloc不会再重复进入这个流程。那么当alloc再次执行到callAlloc的时候就会进入 _objc_rootAllocWithZone(cls, nil) 方法,我们看到return的是 _class_createInstanceFromZone

image.png

进入_class_createInstanceFromZone方法,代码如下

image.png

这里我们看到完整的对象创建流程,并最终返回创建好的obj对象。但是我们看到这里有两处判断我们不知道是什么意思,一处是为对象申请内存空间这个位置,会判断是否传入malloc_zone_t结构体,如果有就执行malloc_zone_calloc,没有就执行calloc,点击进入这两个方法看不到实现,这是因为这两个方法在libmalloc源码内,接下来打开我们下载好的可编译libmalloc源码。创建target和main.m和相关配置,然后main函数内分别写一下这两个方法,分别点击进入,我们会发现这两个方法最终都会调用 _malloc_zone_calloc 方法,只是calloc没有zone参数就给_malloc_zone_calloc函数一个默认的zone。

image.png

image.png

那么我们点进去_malloc_zone_calloc方法看一下,

image.png

我们看到这里返回的ptr就是之前返回的创建完的obj对象,那么这个ptr是通过zone->calloc(zone, num_items, size)调用的,但是这个方法点击去找不到实现了,那我们编译运行一下看看这里究竟是如何调用的,在main函数calloc调用的位置打好断点,然后按住contrl不断地step into instruction,最终我们会看到在zone->calloc(zone, num_items, size)调用处进入到了 default_zone_calloc方法

image.png

我们看到最后又return了zone->calloc(zone, num_items, size)这个东西,那我们继续step into instruction看下这次这个鬼东西执行的是什么,这个时候发现进入了一下参数构造的内部,那我们还是直接看一下这个到底是什么吧,在这个位置打上断点重新运行

image.png

通过断点我们看到这个鬼东西最终执行了 nano_calloc 我们搜一下这个函数,看到这样一个结构

image.png

这里我们看到正常情况下执行_nano_malloc_check_clear返回p,异常情况执行zone->calloc这个鬼东西,那我们打上断点看一下究竟如何执行,断点成功进入_nano_malloc_check_clear函数,看一下这个函数做了什么,发现这个函数好长一大堆,我们来排除一些debug环境代码和一些异常情况看一下

image.png

这里计算了需要申请的内存空间大小,然后申请开辟了内存,到这里对象空间申请完成。

那么我们回到_class_createInstanceFromZone这个方法内部,看一下申请完对象内存空间之后做了什么

image.png

这里我们看到排除异常情况,obj回去初始化isa,这里的两个初始化isa方法最终都会进入initIsa

image.png

我们看一下initIsa函数

image.png

这里就是初始化isa以及赋值了,后面return obj完成对象初始化

总结

整个alloc的流程可以用下面这张流程图来表示

未命名文件.png