Objective-C 的“类”在 Runtime 层面的加载是一个严密的流水线作业。它并不是在 main 函数执行时才加载,而是在 App 启动后的 Pre-main 阶段(即 dyld 加载动态库的过程中)完成从“二进制数据”到“内存对象”的转变。
这个过程主要分为 静态映射、动态修复(Realization) 和 初始化 三个关键时间节点:
1. 静态映射阶段:_read_images (最早时机)
当 dyld 将你的 App 二进制文件映射到内存空间后,libobjc 的 _read_images 函数会被触发。这是类第一次在内存中“露脸”。
- 动作:Runtime 扫描二进制文件的
__DATA段中的__objc_classlist指针数组。 - 状态:此时类已经分配了地址,但它还是一块“死数据”(
class_ro_t)。它虽然在内存里,但你还没法给它发消息,因为它的继承链、方法缓存等都还没准备好。
2. 动态修复阶段:realizeClassWithoutSwift
这是类真正“活过来”的时刻。通常发生在 _read_images 遍历类列表时,或者第一次访问该类时。
-
动作:
- 分配空间:为类对象分配
class_rw_t(可读写数据区)。 - 继承链联结:将
superclass和isa指针指向对应的父类和元类(Meta Class)。 - 分类合并:将该类所有的
Category方法、属性、协议插入到类的方法列表中。
- 分配空间:为类对象分配
-
状态:此时类已经完成了“工业化装配”,可以正常工作了。
3. 初始化阶段:+load 与 +initialize
虽然类结构已经搭好,但类内部的逻辑激活有两个重要的生命周期:
-
+load(Pre-main 阶段) :当类被 Realize(修复)完成后,
libobjc会在main函数之前统一调用所有类的+load。- 特点:此时类刚刚加载进内存,环境还比较原始。
-
+initialize(运行时) :这才是“懒加载”的体现。只有当你的代码第一次向该类发送消息(比如
[MyClass alloc])时,Runtime 才会调用这个方法。
总结:类加载的时间线
| 启动阶段 | 触发函数 | 类的状态 |
|---|---|---|
| dyld 加载镜像 | _read_images | 发现类,建立地址映射(静态数据) |
| Pre-main 阶段 | realizeClass | 内存结构补全,合并分类,连接父类 |
| Pre-main 阶段 | call_load_methods | 执行 +load,类已就绪 |
| Main 之后(首次使用) | lookUpImpOrForward | 执行 +initialize(懒加载初始化) |
💡 一个有趣的细节
如果一个类在整个 App 运行过程中从未被使用过(且没有 +load 方法),为了节省内存,Runtime 可能会推迟它的 Realize 过程。这种**“按需装配”**的机制保证了即使项目中有上万个类,启动速度也不会受到毁灭性的影响。