Runtime 深度剖析:类加载机制与 Category
1. 类加载过程 (Dyld & Objc Setup)
App 启动时,dyld 加载动态库,通知 Runtime (_objc_init) 进行初始化。
1.1 realizeClass
Runtime 会读取 Mach-O 中的类元数据并逐步 realize 类。class_ro_t 保存编译期生成的基础信息,运行时可写结构用于组织动态附加的方法、属性和协议。它不是简单把一个 ro 对象“转换成”另一个 rw 对象;不同 objc4 版本还可能通过扩展结构延迟分配部分可写数据。
ro:编译期确定,包含 ivars, base methods。rw:运行时生成,包含 methods, properties, protocols (可动态添加)。
1.2 +load 方法
- 调用时机:ImageLoader 加载镜像到内存时(main 函数之前)。
- 调用顺序:
- 父类先于子类。
- 类先于 Category。
- 多个 Category 的顺序受 image 加载和链接产物中的排列影响,不应依赖。
- 特点:
- Runtime 会串行协调
+load调度,但+load中自己访问的共享状态仍需正确同步。 - 不走消息转发(直接通过函数指针调用),因此子类不会自动调用父类的 load。
- 可以在此进行 Method Swizzling。
- Runtime 会串行协调
1.3 +initialize 方法
- 调用时机:类进入首次使用路径、Runtime 需要确保其初始化时。常可理解为首次消息发送前,但不要依赖某一条消息作为精确触发点。
- 特点:
- Runtime 会先保证父类完成初始化,再初始化子类,并协调并发线程。
- 每个 Class 的“已初始化状态”只完成一次;如果子类没有覆写
+initialize,继承到的父类实现可能再次以子类作为 receiver 执行,所以同一段方法实现可能被调用多次。 - 不建议在业务代码里主动调用
[SomeClass initialize]。
2. Category 底层原理
Category 允许我们在不继承的情况下给类添加方法。
2.1 实现原理
编译后,Category 的数据被存放在 category_t 结构体中。在 Runtime 加载阶段(attachCategories):
- 获取类原本的方法列表。
- 获取所有 Category 的方法列表。
- 将 Category 的方法、协议和属性元数据附加到类的运行时数据结构。具体使用二维列表、数组还是扩展结构属于 objc4 版本细节。
- 在常见实现中,新附加的方法列表位于查找顺序的前部,因此同名 selector 会优先命中 Category 实现。
2.2 为什么 Category 方法会”覆盖”原类方法?
实际上并没有覆盖(Overwrite)。Category 的方法被放到了方法列表的头部。 方法缓存未命中后,慢速查找会按 Runtime 组织的列表顺序寻找 selector,因此 Category 的同名实现可能优先命中。旧 IMP 并没有从二进制里消失,但这种冲突顺序没有公共 API 保证,不应被当成可靠的“覆盖机制”使用。
2.3 Category 能添加成员变量吗?
- 直接添加:不能。因为类的内存布局 (
instanceSize) 在编译期和ro阶段已经确定。 - 间接实现:使用 关联对象 (Associated Objects) 技术。