9-3.【OC】【Category】Category 冲突的底层原因是什么?

4 阅读3分钟

Category 冲突(即多个 Category 实现了同名方法,导致只有一个生效)的底层原因,源于 Runtime 加载机制中的“覆盖式”内存操作 以及 方法查找的线性遍历逻辑

我们可以从 数据结构加载顺序内存平移 三个层面来拆解:


1. 数据结构:单向列表的特性

在 Objective-C 中,类的方法存储在 class_rw_t 结构体的 methods 列表中。这是一个 二维数组(实际上是多个 method_list_t 的集合)。

当对象接收到消息时,Runtime 会调用 lookUpImpOrForward

  1. 它会 从头到尾 遍历这个二维数组。
  2. 只要匹配到方法名,就立即返回 IMP(函数指针)并缓存。
  3. 关键点:一旦在前面的 Category 列表中找到了方法,它就永远不会再去查找排在后面的原类方法或其他 Category 方法。

2. 底层函数:attachCategories 的操作

当程序启动加载镜像(Images)时,Runtime 会调用 attachCategories 将分类的方法整合进类中。以下是它的核心逻辑伪代码:

C++

// 核心逻辑简述
auto rwe = cls->data()->ext();
// 1. 创建一个新的方法列表容器,大小 = 现有列表 + 准备加入的 Category 列表
// 2. 将 Category 的方法列表放在最前面
// 3. 将原有的方法列表(包括原类和先加载的 Category)向后平移
rwe->methods.attachLists(addedLists, addedCount);

内存布局的变化:

假设有一个类 A,先加载了 Category 1,后加载了 Category 2

  • 初始态[原类方法]
  • 加载 Cat 1 后[Cat 1 方法, 原类方法]
  • 加载 Cat 2 后[Cat 2 方法, Cat 1 方法, 原类方法]

由于采用了 “后入优先” 的插入方式,排在最前面的方法会产生“屏蔽效应”。


3. 冲突的必然性:编译顺序 (Build Phases)

为什么多个 Category 冲突时,某一个会“胜出”?这取决于 编译顺序(Compile Sources)

  • 在 Xcode 的 Build Phases -> Compile Sources 中,文件从上到下按顺序编译。
  • 最后编译 的分类,其 category_t 数据会被 Runtime 最后处理
  • 根据 attachLists 的逻辑,最后处理的分类方法会被插入到二维数组的 最顶端

底层原因总结:Category 冲突不是因为代码被覆盖删除了,而是因为它们在方法列表中的 物理顺序 有先后,而消息查找机制是 “抢占式” 的。


4. 与 +load 方法的特殊区别

值得注意的是,+load 方法是 Category 冲突规则中的 特例

  • 普通方法:遵循上述“覆盖”规则。
  • +load 方法:Runtime 对它有特殊处理。它不会通过消息发送机制调用,而是直接根据函数指针地址,先调用类的 +load,再按编译顺序依次调用所有 Category 的 +load。因此,Category 的 +load 不会覆盖原类的 +load

💡 如何在底层层面“解冲突”?

如果你必须调用被遮蔽的方法,唯一的手段是直接操作 method_list。通过 class_copyMethodList 获取所有同名方法,然后手动跳过第一个 IMP,去执行后面的 IMP。