「OC」源码学习——alloc与init的实现
「OC」源码学习——alloc与init的实现
前言
费劲千辛万苦终于项目给写完了,进入下一个阶段,源码的学习
alloc的调用顺序
我们在main函数之中打上断点,先运行
再在alloc之中的各个函数之中打上断点,在关键步骤上打上断点,我们可以,很容易总结出我们alloc源码的编译流程。
先说结论,编译的具体流程如下
流程探究
接下来我们在按照这个顺序进行断点调试,我们现在alloc
与objc_alloc
处打一个断点,运行程序,我们会发现我们的断电先停在的是objc_alloc
内,查看网上的资料,这是因为LLVM 在编译阶段会将 alloc
符号替换为 objc_alloc
入口点。
接着继续步入,断点情况如下
进入alloc流程之中的核心callAlloc
,对两个return打上断点,我们可以看到,自定义类在第一次进入callAlloc的时候,是会使用消息转发的方法,再去调用真正的alloc
函数
至于为何需要走两次callAlloc
的原因我们先按下不表,接着程序再次调用callAlloc
,这次我们会发现断点打在了_objc_rootAllocWithZone
之中
进入函数看看
这个函数内容过于复杂,等到下一篇文章再详细阐述,我们只要知道,这个程序是动态创建类实例的核心函数,用于分配内存并初始化实例的 isa
指针。
Objective-C 中自定义类的 alloc
方法调用流程涉及双重 callAlloc
的机制,其根源在于 类初始化延迟 和 运行时多态性设计。以下是该流程的核心原因及分步解析:
1. 首次调用 objc_alloc
- 入口:
id objc_alloc(Class cls)
是运行时提供的全局分配函数。 - 目的:
- 检查类是否已初始化(
cls->isRealized()
)。 - 若未初始化,触发
class_initialize()
完成类加载。
- 检查类是否已初始化(
2. 进入 callAlloc
- 参数:
checkNil=true
,allocWithZone=false
。 - 逻辑:
- 检查类是否存在(
slowpath(checkNil && !cls)
)。 - 若类支持优化路径(未重写
allocWithZone:
),直接调用_objc_rootAllocWithZone
。
- 检查类是否存在(
3. 动态派发 objc_msgSend
- 触发条件:若类重写了
allocWithZone:
,需通过消息发送调用自定义逻辑。 - 结果:进入类的
+alloc
方法,最终再次调用callAlloc
。
4. 第二次 callAlloc
- 参数:
checkNil=false
,allocWithZone=true
。 - 逻辑:
- 直接调用
_objc_rootAllocWithZone
,跳过安全检查。 - 最终通过
_class_createInstanceFromZone
完成内存分配和isa
绑定。
- 直接调用
两次进入的区别
我们现在在来看看两次进入callAlloc
究竟有什么区别
第一次调用 callAlloc
时,参数 checkNil=true
,在程序之中需要验证类是否存在。
第二次调用 callAlloc
时,参数 checkNil=false
,跳过安全检查。
其实差别就在于,后面传进的两个布尔值,其实就是是否需要进行安全检查。在第一次进入callAlloc会做一次检查,确保对应的类不为空。
为何自定义类需要进入两次callAlloc
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{if (slowpath(checkNil && !cls)) return nil;if (fastpath(!cls->ISA()->hasCustomAWZ())) {return _objc_rootAllocWithZone(cls, nil);}// No shortcuts available.if (allocWithZone) {return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);}return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}
系统类(如 NSObject
):
因为NSObject在编译时已完成isa
指针的初始化。所以不需要经过两次callAlloc
只需要直接进入objc_alloc
->callAlloc
->_objc_rootAllocWithZone
直接申请空间即可
自定义类:
-
第一次
callAlloc
通过objc_alloc
进入,目的是 检查类的初始化状态,类初次创建是没有默认的alloc/allocWithZone
实现的所以继续向下执行进入到msgSend
消息发送流程,为这个类进行初始化。 -
第二次
callAlloc
,此时类已初始化完成,进入实际内存分配逻辑。 -
callAlloc
的分支逻辑:if (fastpath(!cls->ISA()->hasCustomAWZ())) {return _objc_rootAllocWithZone(cls, nil); // 直接分配内存 } else {return objc_msgSend(cls, @selector(alloc)); // 通过消息发送调用自定义逻辑 }
hasCustomAWZ()
:检测类是否重写了allocWithZone:
方法。- 若未重写(默认情况),直接调用
_objc_rootAllocWithZone
分配内存。 - 若重写,则通过
objc_msgSend
触发动态派发,确保调用正确的alloc
实现。
那么源码是如何查看缓存的呢,我们进入
hasCustomAWZ()
看一下# define FAST_CACHE_HAS_DEFAULT_AWZ (1<<14) bool hasCustomAWZ() const {return !cache.getBit(FAST_CACHE_HAS_DEFAULT_AWZ);}bool getBit(uint16_t flags) const {return _flags & flags;}
FAST_CACHE_HAS_DEFAULT_AWZ
宏定义表示 是否实现alloc/allocWithZone
位域标识位为了证明是类没有初始化的问题才导致
callAlloc
函数在类第一次被调用的时候被进入两次,我们可以通过在申请一次空间来尝试一下,断点和之前的一样。GGObject *obj = [GGObject alloc]; GGObject *obj2 = [GGObject alloc];
我们发现obj2的alloc调用顺序就和NSObject一样了。
objc_alloc
->callAlloc
->_objc_rootAllocWithZone
但紧接着问题又来了,重复进入callAlloc的原因我们找到了,那为什么要设置这个流程,这个所谓的类初始化实在哪里开始的呢?
查看
hasCustomAWZ()
的调用栈可以看到,有个名字为_objc_msgSend_uncached
的方法,其实也很好理解,就是消息发送给了一个初始化信息的类之中,其实就是在消息转发这一步顺带把第一次调用的类进行初始化,然后在第二次进入hasCustomAWZ()
就能进入_objc_rootAllocWithZone
双重 callAlloc
流程是 Objective-C 运行时与编译器协作的产物,其核心目的是 确保类初始化完成 和 兼容自定义内存分配逻辑。这一机制平衡了性能、灵活性与安全性,是 Objective-C 动态特性的典型体现。
init
其实点进去init, 很简单,就是把内容返回一次,如此设计的原因是为了让整个程序符合工厂模式的设计理念,我们可以通过简单的重新init的方法去实现较为复杂的其他操作。
总结
其实我是想讲整一个alloc的流程梳理完整再将内容发出,无奈一个完完整整的流程涉及的内容实在过多,有想要了解的可以看 OC对象底层内存开辟和实现(中)了解初始化类的完整流程
参考资料
iOS-底层原理 02:alloc & init & new 源码分析
iOS-底层原理 04:NSObject的alloc 源码分析
OC对象底层内存开辟和实现(中)