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 函数之前)。
  • 调用顺序
    1. 父类先于子类。
    2. 类先于 Category。
    3. 多个 Category 的顺序受 image 加载和链接产物中的排列影响,不应依赖。
  • 特点
    • Runtime 会串行协调 +load 调度,但 +load 中自己访问的共享状态仍需正确同步。
    • 不走消息转发(直接通过函数指针调用),因此子类不会自动调用父类的 load。
    • 可以在此进行 Method Swizzling。

1.3 +initialize 方法

  • 调用时机:类进入首次使用路径、Runtime 需要确保其初始化时。常可理解为首次消息发送前,但不要依赖某一条消息作为精确触发点。
  • 特点
    • Runtime 会先保证父类完成初始化,再初始化子类,并协调并发线程。
    • 每个 Class 的“已初始化状态”只完成一次;如果子类没有覆写 +initialize,继承到的父类实现可能再次以子类作为 receiver 执行,所以同一段方法实现可能被调用多次。
    • 不建议在业务代码里主动调用 [SomeClass initialize]

2. Category 底层原理

Category 允许我们在不继承的情况下给类添加方法。

2.1 实现原理

编译后,Category 的数据被存放在 category_t 结构体中。在 Runtime 加载阶段(attachCategories):

  1. 获取类原本的方法列表。
  2. 获取所有 Category 的方法列表。
  3. 将 Category 的方法、协议和属性元数据附加到类的运行时数据结构。具体使用二维列表、数组还是扩展结构属于 objc4 版本细节。
  4. 在常见实现中,新附加的方法列表位于查找顺序的前部,因此同名 selector 会优先命中 Category 实现。

2.2 为什么 Category 方法会”覆盖”原类方法?

实际上并没有覆盖(Overwrite)。Category 的方法被放到了方法列表的头部。 方法缓存未命中后,慢速查找会按 Runtime 组织的列表顺序寻找 selector,因此 Category 的同名实现可能优先命中。旧 IMP 并没有从二进制里消失,但这种冲突顺序没有公共 API 保证,不应被当成可靠的“覆盖机制”使用。

2.3 Category 能添加成员变量吗?

  • 直接添加:不能。因为类的内存布局 (instanceSize) 在编译期和 ro 阶段已经确定。
  • 间接实现:使用 关联对象 (Associated Objects) 技术。