iOS底层原理:OC对象底层探索之alloc初探

iOS开发的小伙伴们对 [XXX alloc] init] 都不陌生,可以说 allocinit 贯穿我们整个的开发过程中。那么在OC对象的底层,到底做了哪些操作呢?今天我们就来探索一下 alloc 底层的工作流程。

一、抛砖引玉

我们先来看一下下面这张图中的测试代码和打印结果:

从上面的打印结果来看,p、p1、p2对象的内存地址是一样的,但是 p、p1、p2对象的指针地址( &p、&p1、&p2 )是不同的。而**pNew对象的内存地址和指针地址和 p、p1、p2都不一样,很显然, pNew**属于拥有另一块内存空间的另一个对象了。 由此我们暂时得出结论:

  • **p、p1、p2**对象的指针地址是不同的, 但是他们都指向同一内存空间;
  • alloc 可以开辟内存空间,而 init 不会;
  • p、p1、p2对象的指针地址 &p > &p1 > &p2 > &pNew ,说明栈区是由高到低连续开辟的
  • p 、pNew对象的内存地址 p < pNew ,说明堆区是由低到高开辟内存的

结合堆栈的知识 ,我画了下面👇这张图,帮助大家理解。

二、准备工作

通过上面我们可以发现,对象内存地址是通过 alloc 创建,我们看一下 alloc 是怎么实现的。 点击 alloc 方法进入 NSObject.h :

进入**NSObject.h,我们再点击跳转,发现跳转不进去了,也就看不到 alloc**的实现了。难道我们就只能停在这里?就只能在外面蹭一蹭了吗? NO,下面来介绍一下探索底层的三种方法,方便我们在探索底层源码的时候能够顺利的跟对方法(函数)的一个执行流程。

第一种:添加符合断点方式
  • 在工程中选择断点 --> 点击左下角"+ " --> Symbolic Breakpoint
  • 比如我这里想知道**alloc源码位置, 那么就输入 alloc**
  • 然后运行, 我们发现**alloc**的符号断点非常多,到底哪个才是我们想要的呢?
  • 接着我们还需要在想要执行的代码处增加一个普通断点,比如我们这里在**JQPerson alloc处打上一个断点,然后将 alloc**符号断点先禁用
  • 运行程序,首先来到我们的普通断点**[JQPerson alloc]处,然后我们将符号断点alloc**启用,点击断点操作按钮进入下一步

到这里,我们可以看到**alloc方法在 libobjc.A.dylib**库中(ps:libobjc.A.dylib是objc的系统库,感兴趣的小伙伴可以去苹果开源官网Open Source下载查看,注意: Open Source上下载下来的源码是不能直接编译和调试的,想要下载的objc源码可编译调试的小伙伴可以移步到我之前的文章iOS底层原理(一):苹果开源 objc4-818 源码项目的编译和调试

第二种: 断点 + step into方式
  • 我们先在要执行的代码打上断点,运行项目,来到断点位置
  • 然后按住control 键,点击setp into一步一步查找,会看到如下结果
  • 最后再添加**objc_alloc符号断点,点击Continue program execution**继续执行

这里我们可以看到,断点进入了**libobjc.A.dylib中的 objc_alloc函数,由此可知 alloc方法的源码在 libobjc.A.dylib**库中。

第三种: 汇编跟进方式
  • 首先,我们还是先在要执行的代码打上断点
  • 然后在Xcode菜单栏找到 Debug ==> Debug Workflow ==> Always Show Disassembly并选中(这里是启用汇编进行调试)
  • 运行项目,来到如下图的断点处
  • 我们可以看到当前断点下面两行处,有个**callq xxxx; symbol stub for objc_alloc,接着我们再添加一个 objc_alloc符号断点, 点击 Continue program execution继续执行(ps:这里解释一下: callq是汇编中的一个指令,代表这个这里即将要调用一个方法, symbol stub for objc_alloc翻译过来是 objc_alloc的符号存根,也就是说 objc_alloc**是要调用的方法名)

好了,到此底层探索的三种方式就介绍完了,接下来我们步入正题吧!

三、alloc源码探索

好的,有了上面的探索方法,我们现在就拿 objc 源码项目来探索 alloc 的底层实现吧。 首先,打开之前编译好的 objc4-818.2 项目,需要的小伙伴可以参考我之前文章**iOS底层原理(一):苹果开源 objc4-818 源码项目的编译和调试,到 Open Source 上下载源码自行编译,不想麻烦的也可以直接去 GitHub 上下载:JQObjc4-818.2BuildDebug。 然后,找到 JQObjcBuildDemo 目录下创建一个 JQPerson类。然后在main.m**中添加如下代码:

注意: 这里 16、17行 分别有个断点,后面会用到!!!

我们从上面的底层探索方式中可以看到:[JQPerson alloc]在底层libobjc.A.dylib库中执行的objc_alloc方法,接下来我就来验证一下。

第1步:alloc 和 objc_alloc
    1. 点击**alloc跳转到 objc 的源码中,搜索一下 objc_alloc,然后分别在 alloc objc_alloc**处打上断点

    1. 然后,先将源码中**alloc objc_alloc**处的断点禁用,运行项目来到main.m中的断点处
    1. 接着,启用源码中**alloc objc_alloc处的断点,点击下一步,这时会发现:断点来到了 objc_alloc**处

这就验证了我们前面所讲的,alloc方法在底层libobjc.A.dylib库中执行的objc_alloc方法

    1. 再次点击下一步,惊奇的发现:断点来到了**alloc**方法处

那么为什么**[JQPerson alloc]在底层会先走 objc_alloc方法,再走 alloc方法呢?按照我们在 objc 源码中看到的方法调用流程,应该是 [JQPerson alloc] => alloc**呀?

为了验证这个问题,我们需要请出YYDS(永远滴神):llvm源码 (是苹果开源的系统级别的源码),看一看苹果是不是在这里面做了什么骚操作。llvm-project下载地址

第2步:llvm-project 底层分析

由于 llvm-project 项目比较大,这里我们用 VSCode 打开

    1. 首先,我们全局搜索一下**alloc或者 OMF_alloc:,来到 tryGenerateSpecializedMessageSend**方法,这个方法在 CGObjC.cpp 文件中

我们主要看3号位置的方法解释,这里我翻译了一下,大家可以自行去看,这是苹果对性能的一个优化。主要意思就是:objc在运行时提供了快捷入口,这些入口比普通的消息发送速度更快,如果运行支持所需要的入口的话,这个方法就会调用并返回结果,否则返回None,调用者自己生成一个消息发送。

    1. 知道了**tryGenerateSpecializedMessageSend的作用,接着我再来看一下 tryGenerateSpecializedMessageSend方法的调用情况,搜索 tryGenerateSpecializedMessageSend,来到 GeneratePossiblySpecializedMessageSend**

这个方法是运行时在底层的入口,所有的消息发送都会走这里。从代码可以看出,如果**tryGenerateSpecializedMessageSend方法返回 None,这里判断为 false,就会走 GenerateMessageSend方法,也就是调用者自己生成一个普通的 msgSend**。

    1. 然后,我们深入到**tryGenerateSpecializedMessageSend方法中,看看 alloc是怎么被执行成了 objc_alloc。这里看一下 tryGenerateSpecializedMessageSend方法中4号位置的代码,这里有个条件判断 if (Sel.isUnarySelector() && Sel.getNameForSlot(0) == "alloc") ,如果成立,就会走 EmitObjCAlloc**方法,搜索一下,进去看一下

可以看到**EmitObjCAlloc方法这里生成了一个 objc_alloc的入口( ObjCEntrypoints),包装为 emitObjCValueOperation被返回执行,并且 llvm对此做一个标记存在 Selector中,而 Selector则记录在 SelectorTable**中

由此可以验证:[JQPerson alloc]在底层会先走到 objc_alloc

    1. objc_alloc第一次调用 callAlloc方法,会执行 msgSend(cls, @selector(alloc)) (ps:这个第3步 callAlloc中会讲,这里知道一下,先把 llvm这个流程讲完)。 此时 llvm底层还是会走 tryGenerateSpecializedMessageSend ,此时,由于已经标记了**allocSelector,不会再走 *if (Sel.isUnarySelector() && Sel.getNameForSlot(0) == "alloc")*这个判断中的代码,最终返回None。然后由 GenerateMessageSend**走普通的消息发送。
第3步:callAlloc

好了,alloc objc_alloc的调用清晰了。接着,我们来看一下最核心的方法 callAlloc

    1. objc 源码中我们可以看到**objc_alloc方法中调用了 callAlloc**
    1. 我们观察一下**callAlloc中的代码,会发现这个方法的最后一行(1937行)对传入的 cls 做了一次消息发送,发送的消息名称正是 alloc,这似乎可以解释上面走完 objc_alloc方法后,又走到 alloc**的现象。但是我们还需要打断点,走一下流程来验证。
    1. 经断点调试,执行**objc_alloc后, callAlloc确实走到了发送 alloc消息这一行,也就是 [JQPerson alloc] => objc_alloc => callAlloc**
    1. 继续走断点,我们会发现执行流程为:[JQPerson alloc] => objc_alloc => callAlloc => alloc => _objc_rootAlloc => callAlloc =>_objc_rootAllocWithZone

    1. 当前我们已经走完了 main.m 中的16行,也就是**(JQPerson)p1 alloc**,此时断点会来到17行 (JQPerson)p2 alloc
    1. 继续走源码断点,会发现执行流程为:[JQPerson alloc] => objc_alloc => callAlloc => _objc_rootAllocWithZone

这里我们就会奇怪,为什么**JQPerson类再次 alloc时,就直接走到 *if (fastpath(!cls->ISA()->hasCustomAWZ()))***条件判断中的代码了呢?

    1. 那我们就来看一下***if (fastpath(!cls->ISA()->hasCustomAWZ()))这句判断到底执行了什么? 进入到 if (fastpath(!cls->ISA()->hasCustomAWZ()))***源码中看一下

由以上源码可以看出: a. 当**JQPerson类第一次调用 alloc方法时,底层会先调用 objc_alloc,此时 callAlloc被第一次调用, callAlloc内部通过当前 cls ISA返回一个 Class对象; b. 紧接着会去判断当前 Class cache FAST_CACHE_HAS_DEFAULT_AWZ(存储在元类 metaclass中,记录着 cache中是否已经缓存了 alloc/allocWithZone:方法的实现)这个标志位的值是否为真,由于是第一次执行,没有缓存,所以 cache.getBit(FAST_CACHE_HAS_DEFAULT_AWZ)取出来的值是 false,前面加个 ,变成了 true callAlloc if (fastpath(!cls->ISA()->hasCustomAWZ()))又加了个 ,所以值为 false; c. 然后走到了 if (allocWithZone),由于 objc_alloc方法中 allocWithZone参数传值为 false,所以走到了 (objc_msgSend)(cls, @selector(alloc))。然后, callAlloc被第二次调用,由于执行过了 alloc方法,所以此时有了 alloc的方法缓存,所以 if (fastpath(!cls->ISA()->hasCustomAWZ()))判断为 true,执行 _objc_rootAllocWithZone。 d. 最后就是 main.m 中第17行 JQPerson类第二次调用 alloc方法,此时由于 JQPerson类的 cache中已经有了缓存, FAST_CACHE_HAS_DEFAULT_AWZ这个标志位的值为真,也就是 if (fastpath(!cls->ISA()->hasCustomAWZ()))这个条件为真,所以,会直接执行 _objc_rootAllocWithZone**。 下面我画一下流程图,帮助小伙们理解一下:

另外,这里我附一张**NSObject alloc]**的流程图,有兴趣的小伙们可以去试一试:

这里**NSObject alloc]只走了一遍 callAlloc方法,猜测原因是:系统对 NSObject 做了优化,提前给 cache**添加了缓存。

好了,**alloc的底层探索今天先写到这里。下面一篇文章我们将探索一下 alloc**开辟内存空间相关的源码。敬请期待吧!!!

相关推荐
Digitally4 小时前
如何准备 iPhone 以旧换新:10 个重要提示
ios·iphone
Tlaster5 小时前
使用KMP实现原生UI + Compose混合的社交客户端
android·ios·开源
游戏开发爱好者86 小时前
iOS 26 App 查看电池寿命技巧,多工具组合实践指南
android·macos·ios·小程序·uni-app·cocoa·iphone
linghugoogle9 小时前
基于 Metal 的 iOS 全景视频播放器
ios
库奇噜啦呼11 小时前
【iOS】自动引用计数(一)
macos·ios·cocoa
游戏开发爱好者813 小时前
iOS 混淆工具链实战 多工具组合完成 IPA 混淆与加固 无源码混淆
android·ios·小程序·https·uni-app·iphone·webview
2501_9160088919 小时前
用多工具组合把 iOS 混淆做成可复用的工程能力(iOS混淆|IPA加固|无源码混淆|Ipa Guard|Swift Shield)
android·开发语言·ios·小程序·uni-app·iphone·swift
胎粉仔19 小时前
Swift 初阶 —— inout 参数 & 数据独占问题
开发语言·ios·swift·1024程序员节