【iOS】alloc的实际流程
目录
前言
为什么不按源码流程调用?
alloc的调用流程
前言
在之前的博客中我们有学习到过alloc的底层原理,沿着源码一步步找到了alloc的调用链——alloc—>_objc_rootAlloc—>callAlloc—>_objc_rootAllocWithZone—>_class_createInstanceFromZone,但其实在实际的alloc过程中,并不是这个调用流程,如果对NSObject的alloc加上断点调试就会发现,alloc流程并没有进入源码,接下来我们来探究一下为什么会这样以及真实的调用流程。
为什么不按源码流程调用?
在实际运行中,
[NSObject alloc]
或[MyClass alloc]
这类调用通常不会真正进入 libobjc 的源码层(比如_objc_rootAlloc
),而是走了更加高效的底层路径,这是由于 Apple 对运行时做了大量优化(比如汇编级别快速路径、ISA-optimized fast path)来避免频繁进入 C 层函数。
1.对于非NSObject类, objc_msgSend是汇编函数,非普通 C 函数
-
它大多数时候在汇编层 直接查找 IMP 并跳转执行,不会进入 Objective-C runtime 的 C 函数实现。
-
也就是说,objc_msgSend(obj, @selector(alloc)) 通常直接跳到了元类中的 +alloc 的 IMP。(这里涉及到后面cache_t的方法缓存,如果命中了Cache,就不会走完整的流程)
-
对于NSObject, 是基础类,系统做了特殊优化
-
对于 NSObject和一些常见类,Apple 使用了 汇编级别的 fast path,这意味着即便你打断点试图进入 _objc_rootAlloc,你可能根本进不去。
-
常见的“未命中源码”的情况说明使用的是缓存或特殊入口。
alloc的调用流程
通过调用alloc后的堆栈详情我们就可以发现,无论是NSObject类还是自定义类,调用alloc方法最开始走的都是objc_alloc而不是objc_rootAlloc。这是因为消息转发时系统在底层帮我们转发到了objc_alloc。我们来看看objc_alloc的源码实现
可以发现其实他和objc_rootAlloc的实现是一样的,调用callAlloc。
回顾之前callAlloc的实现
可以看到callAlloc中分为几个分支来处理
对于NSObjcet类,初始化在llvm编译时就已经初始化好了,因此缓存中已经有alloc/allocWithZone方法了,hasCustomAWZ()为false,那么!cls->ISA()->hasCustomAWZ()就为true。
因此NSObjcet在此时会进入_objc_rootAllocWithZone并调用_class_createInstanceFromZone,后面的步骤就和之前说的一样了,这就是为什么NSObjct没有走alloc方法。
而对于自定义类,初次创建时没有默认的alloc/allocWithZone实现,所以继续向下执行进入到消息发送流程,消息转发时会向父类找,最终找到NSObjcet的alloc并调用,即[NSObjcet alloc],这时会来到_objc_rootAlloc,进入后再次调用callAlloc,这次调用的是NSObject类的,缓存中存在alloc/allocWithZone实现,就接着走_objc_rootAllocWithZone方法,后面步骤也就和之前一样了。所以自定义类在调用alloc时会走两次callAlloc。
总结一下,NSObjcet的调用链如下图:
自定义类的调用链如下图: