9-2.【OC】【Category】Category 方法覆盖原有方法的规则是什么?

4 阅读3分钟

在 Objective-C 中,Category 覆盖原类方法的规则是面试中的高频考点。理解这个规则的关键在于:这不是真正的“覆盖(Override)”,而是“遮蔽(Shadowing)”。

以下是底层运行时的处理规则及原理:


1. 核心规则:后编译者胜

当一个 Category 实现了与原类(Class)同名的方法时:

  1. 原类方法依然存在:原类的方法并没有被删除或替换,它仍然保留在类的 method_list 中。
  2. 顺序决定调用:在运行时,Category 的方法会被插入到类的方法列表 最前面
  3. 消息转发机制:当对象接收到消息(如 [obj method])时,运行时系统会遍历方法列表。由于 Category 的方法排在前面,系统找到它后就会立即执行并停止查找。
  4. 同名 Category 冲突:如果多个 Category 实现了同一个方法,**最后编译(Last Compiled)**的那个 Category 的方法会排在最前面,从而被调用。

2. 底层原理:attachCategories

Category 的方法是在 Runtime 初始化阶段 动态加载的。其过程如下:

  • 编译后的 Category 包含一个 category_t 结构体,存储了新增的方法列表。
  • 在程序启动时(realizeClassWithoutSwift 阶段),Runtime 会调用 attachCategories 函数。
  • 该函数会通过 memmove 将原类的方法列表向后移动,腾出前面的空间,然后将所有 Category 的方法列表 memcpy 到空出来的起始位置。

关键点: 因为是往“头部”插入,所以 Category 的优先级永远高于原类。


3. 产生的影响与限制

A. 无法通过 super 调用原类实现

在正常的子类继承中,你可以用 [super method] 调用父类。但在 Category 中,self 的父类依然是原类的父类。

  • 如果在 UIView 的 Category 里重写了 layoutSubviews,并在内部调 [super layoutSubviews],你调用的是 UIScrollView(或其父类)的实现,永远无法调用到 UIView 本身被遮蔽的那个实现

B. 调试困难

当方法被遮蔽后,断点或堆栈信息可能会显示非预期的路径,增加排查难度。


4. 进阶:如何调用被“覆盖”的原类方法?

虽然直接调用行不通,但因为原类方法还在列表里,我们可以通过底层手段找到它:

Objective-C

// 这种方式可以绕过 Category,直接找到原类的方法实现
Class currentClass = [MyClass class];
uint count;
Method *methodList = class_copyMethodList(currentClass, &count);

for (int i = 0; i < count; i++) {
    Method method = methodList[i];
    SEL selector = method_getName(method);
    if (selector == @selector(myMethod)) {
        // 最后一个通常是原类的实现(因为 Category 的在前面)
        // 实际操作中可以通过判断 implementation 地址来精确查找
    }
}
free(methodList);

5. 开发建议:命名规范

由于这种“遮蔽”机制非常霸道且难以追溯,最佳实践是:

  • 增加前缀:为 Category 中的方法名统一增加前缀(例如 lx_myMethod),这能彻底规避与原类或第三方库的命名冲突。
  • 避免重写:除非是做 AOP(面向切面编程)或者特殊 Hook,否则尽量不要在 Category 中实现原类已有的方法。

💡 深度思考

既然 Category 是通过修改方法列表来实现“覆盖”的,这与 Method Swizzling(方法交换) 相比有什么区别?如果你想在不破坏原类逻辑的前提下增加功能,你会选择哪种方案?