在 Runtime 层面,Category 和 Extension 的差异本质上是 “静态合并” 与 “动态注入” 的博弈。这种差异直接决定了它们如何影响类的方法列表。
1. Extension:编译期的静态合并
Extension 的方法在 编译阶段 就已经确定了。
-
处理机制:编译器在编译原类(Class)时,会自动将 Extension 中的方法合并到原类的
class_ro_t(Read-Only Data)结构体的方法列表中。 -
对方法列表的影响:
- 内存布局:Extension 的方法和原类方法在内存中是连在一起的,处于同一个
method_list_t。 - 查询效率:由于是静态合并,Runtime 不需要额外的逻辑来处理 Extension。在消息转发查找方法时,Extension 的方法和普通方法没有区别。
- 不可替代性:因为数据写死在了
class_ro_t里,你无法在运行时通过普通的内存操作移除 Extension 添加的方法。
- 内存布局:Extension 的方法和原类方法在内存中是连在一起的,处于同一个
2. Category:运行时的动态注入
Category 的方法是在 程序启动(Runtime 加载镜像) 时才“挂载”上去的。
-
处理机制:
- 编译器将 Category 编译成独立的
category_t结构体。 - Runtime 在初始化类时,调用
attachCategories函数。 - Runtime 会动态创建一个新的二维数组(
method_array_t),将 Category 的方法列表插入到原类方法列表的前面。
- 编译器将 Category 编译成独立的
-
对方法列表的影响:
- 顺序优先:Category 的方法会排在方法列表的最前端。根据消息传递(
objc_msgSend)的线性查找规则,同名方法下,Category 会优先被命中,产生“覆盖”原类方法的现象。 - 动态性:Category 的方法存在于
class_rw_t(Read-Write Data)中。这意味着理论上你可以在运行时通过 Runtime API 动态地修改或替换这些方法。 - 冲突风险:如果多个 Category 实现了同名方法,方法列表会变得非常臃肿,且只有最后被加载的那个 Category 方法能被正常查找到。
- 顺序优先:Category 的方法会排在方法列表的最前端。根据消息传递(
3. 核心差异对比表
| 特性 | Extension (类扩展) | Category (分类) |
|---|---|---|
| 影响的结构体 | class_ro_t (只读,编译期确定) | class_rw_t (可读写,运行时确定) |
| 方法位置 | 与原类方法混在一起,无固定先后 | 强制插入到列表头部,优先级最高 |
| 方法列表性质 | 一维数组(合并后的结果) | 二维数组(原类列表 + 多个分类列表) |
| 编译产物 | 直接合并进类的 DATA 段 | 生成独立的 category_t 结构体 |
| 性能损耗 | 无(等同于原生方法) | 启动时有加载损耗,列表过长会影响查找速度 |
4. 深度洞察:为什么 Category 容易影响性能?
由于 Category 采用的是 “头部插入” 的方式,当一个类拥有大量的 Category 时:
- 方法查找变慢:
objc_msgSend在缓存未命中时,必须遍历这个庞大的二维方法列表。 - 启动时间增加:Runtime 在
map_images阶段需要花费大量 CPU 时间来搬运和重组这些方法列表(即attachLists操作)。
💡 总结建议
如果你拥有源码,且扩展是私有的,请务必使用 Extension,因为它不仅能添加成员变量,而且对 Runtime 方法查找没有任何额外负担。只有当你需要扩展系统类,或者需要将大型类按功能模块化拆分时,才考虑使用 Category。