在 Objective-C 中,Category 覆盖原类方法的规则是面试中的高频考点。理解这个规则的关键在于:这不是真正的“覆盖(Override)”,而是“遮蔽(Shadowing)”。
以下是底层运行时的处理规则及原理:
1. 核心规则:后编译者胜
当一个 Category 实现了与原类(Class)同名的方法时:
- 原类方法依然存在:原类的方法并没有被删除或替换,它仍然保留在类的
method_list中。 - 顺序决定调用:在运行时,Category 的方法会被插入到类的方法列表 最前面。
- 消息转发机制:当对象接收到消息(如
[obj method])时,运行时系统会遍历方法列表。由于 Category 的方法排在前面,系统找到它后就会立即执行并停止查找。 - 同名 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(方法交换) 相比有什么区别?如果你想在不破坏原类逻辑的前提下增加功能,你会选择哪种方案?