Runtime 深度剖析:类加载机制与 Category
1. 类加载过程 (Dyld & Objc Setup)
App 启动时,dyld 加载动态库,通知 Runtime (_objc_init) 进行初始化。
1.1 realizeClass
Runtime 会解析 Mach-O 文件中的数据,将磁盘上的 class_ro_t (Read Only) 转换为内存中的 class_rw_t (Read Write)。
ro:编译期确定,包含 ivars, base methods。rw:运行时生成,包含 methods, properties, protocols (可动态添加)。
1.2 +load 方法
- 调用时机:ImageLoader 加载镜像到内存时(main 函数之前)。
- 调用顺序:
- 父类先于子类。
- 类先于 Category。
- 多个 Category 之间取决于编译顺序。
- 特点:
- 线程安全。
- 不走消息转发(直接通过函数指针调用),因此子类不会自动调用父类的 load。
- 可以在此进行 Method Swizzling。
1.3 +initialize 方法
- 调用时机:类第一次接收到消息时(懒加载)。
- 特点:
- 走完整的消息发送流程 (
objc_msgSend)。 - 线程安全(Runtime 内部加锁)。
- 如果子类没实现,会调用父类的(可能导致父类的 initialize 跑多次)。
- 走完整的消息发送流程 (
2. Category 底层原理
Category 允许我们在不继承的情况下给类添加方法。
2.1 实现原理
编译后,Category 的数据被存放在 category_t 结构体中。在 Runtime 加载阶段(attachCategories):
- 获取类原本的方法列表。
- 获取所有 Category 的方法列表。
- 重新分配内存:创建一个新的大数组。
- 数据移动:
- 将 Category 的方法拷贝到新数组的前面。
- 将类原本的方法拷贝到新数组的后面。
- 将新数组赋值给
class_rw_t。
2.2 为什么 Category 方法会”覆盖”原类方法?
实际上并没有覆盖(Overwrite)。Category 的方法被放到了方法列表的头部。 消息发送时,遍历方法列表,一旦找到匹配的方法就返回。因此,处于头部的 Category 方法被优先执行了。
2.3 Category 能添加成员变量吗?
- 直接添加:不能。因为类的内存布局 (
instanceSize) 在编译期和ro阶段已经确定。 - 间接实现:使用 关联对象 (Associated Objects) 技术。