在 Objective-C 中,类的加载并不是简单的“一次性到位”,而是一个从磁盘二进制文件到运行时动态对象的深度加工过程。
这个过程主要由 dyld(动态链接器)和 libobjc(Runtime 库)协作完成,大致可以分为以下 5 个阶段:
1. 静态映射阶段 (Mapping)
当 App 启动时,dyld 会将 App 的可执行文件(Mach-O)和依赖的动态库加载到虚拟内存中。
- 数据来源:此时类的信息存储在 Mach-O 文件的
__DATA段中(如__objc_classlist、__objc_methname等)。 - 结果:此时类只是内存中的一堆“静态字节”,指针尚未重定位。
2. 注册与读取阶段 (_read_images)
这是 libobjc 介入的第一个关键点。dyld 会通知 Runtime:“镜像(Image)已就位,请处理”。
- Sel 注册:将所有的
SEL(方法名)注册到全局哈希表中,确保内存中同一个方法名只有一份地址。 - 类注册:Runtime 扫描所有定义的类,将它们映射到内部的类列表(gdb_objc_realized_classes)中。
- 协议修复:处理所有
@protocol。
3. 实现与修复阶段 (Realization)
这是最核心的步骤。此时类由 class_ro_t(只读数据)升级为 class_rw_t(可读写数据)。
- 分配空间:在堆上为类分配
class_rw_t结构。 - 继承链修正:递归地去实现父类和元类(Meta Class)。将
superclass和isa指针指向对应的类对象。 - 方法列表处理:将
class_ro_t中的原始方法、属性、协议拷贝到class_rw_t中。
4. 分类合并阶段 (Category Attachment)
在类实现之后,Runtime 会处理该类关联的所有 Category(分类) 。
- 逻辑:将分类中的方法、属性、协议列表提取出来。
- 插入顺序:将分类的方法列表插入到主类方法列表的前面。
- 结果:这就是为什么分类中如果有同名方法,会“覆盖”主类方法(本质上是优先找到了分类的方法)。
5. 初始化阶段 (Initialization)
这是逻辑层面的激活。
- +load 调用:在
main函数之前,Runtime 会遍历所有类,按照“父类 -> 子类 -> 分类”的顺序调用+load。 - +initialize 调用:这是“懒加载”。当类第一次收到消息(即第一次调用方法)时,Runtime 会检查该类是否初始化过,如果没有,则执行
+initialize。
总结全景图
| 阶段 | 执行者 | 核心产物 | 时间点 |
|---|---|---|---|
| 映射 | dyld | 原始 Mach-O 数据 | Pre-main |
| 读取 | libobjc | 符号哈希表、初始类表 | Pre-main |
| 实现 | libobjc | class_rw_t 结构体 | Pre-main |
| 合并 | libobjc | 包含分类的完整方法表 | Pre-main |
| 逻辑激活 | App | +load / +initialize | Pre-main / 首次使用 |