当 Category(分类) 和 Swizzling(方法交换) 同时出现时,系统的调用逻辑会变得非常微妙。要理清顺序,我们需要将“方法查找机制”和“Swizzling 带来的指针偏移”结合起来看。
我们可以从以下两个维度来拆解这个过程:
1. 第一阶段:Category 的“覆盖”效应 (编译与加载期)
在 Swizzling 发生之前,Category 的加载机制首先决定了方法列表的状态。
- 原理:如果一个 Category 实现了与原类同名的方法,Runtime 在加载 Category 时,会将其方法列表插入到原类方法列表的最前面。
- 结果:当
objc_msgSend查找方法时,会优先找到 Category 中的实现。原类的方法依然存在,但被“隐藏”在列表后方,形成所谓的“覆盖”。
2. 第二阶段:Swizzling 的“交换”效应 (执行期)
当你在 +load 中执行 Swizzling 时,具体的调用顺序取决于你交换的是哪两个 SEL。
场景 A:Swizzle 一个 Category 中不存在的方法名(推荐做法)
如果你创建了一个新 SEL(如 my_viewWillAppear:)来交换系统的 viewWillAppear::
-
查找:
class_getInstanceMethod会在方法列表中查找。如果 Category 实现了viewWillAppear:,它拿到的就是 Category 的IMP;否则拿到的是原类的IMP。 -
交换:它将
viewWillAppear:与my_viewWillAppear:的指针对调。 -
调用顺序:
- 调用
viewWillAppear:执行你的 Swizzle 实现。 - 在 Swizzle 实现中调用
[self my_viewWillAppear:]执行 Category 的实现(或原类实现)。
- 调用
场景 B:Category 本身重写了方法且逻辑不全(高风险)
如果 Category A 重写了 method,而你在 Category B 中 Swizzle 了 method:
- 风险点:如果你没有在 Swizzle 代码中调用
[self swizzled_method],那么原类和 Category A 的逻辑都会被彻底截断。
3. 调用链条全景图
假设我们在 UIViewController 上有一个 Category,并且又做了一次 Swizzling。
最终的调用顺序如下:
- 外部触发:
[obj viewWillAppear:] - 第一站(Swizzle 钩子) :进入你 Swizzle 后的方法实现。在这里你可以做埋点、日志等。
- 第二站(回调原 SEL) :你在代码中通过
[self my_viewWillAppear:]触发原实现。 - 第三站(Category 实现) :如果存在同名 Category 方法,此时会执行 Category 的代码。
- 第四站(手动调用原类) :如果 Category 内部写了
[super viewWillAppear:](注意:Category 直接重写通常无法调用原类同名方法,除非使用特殊手段),才会回到原类(Base Class) 。
4. 关键点:如何保证顺序?
由于 Category 和 Swizzling 的加载都发生在 +load,顺序由以下因素决定:
- 编译顺序:在 Xcode 的
Build Phases -> Compile Sources中,排在后面的 Category 会覆盖前面的同名方法。 - 依赖关系:如果 Swizzling 代码写在某个特定的 Category 里的
+load中,它会在该 Category 被挂载到类之后执行。
警告:如果多个 Category 都 Swizzle 了同一个方法,且其中一个 Category 又通过同名覆盖了该方法,会导致链条断裂。
5. 2026 年的开发建议:避开 Category 同名覆盖
在现代开发中,为了保证调用顺序可控,绝不要在 Category 中直接重写(Override)系统已有的方法。
正确的姿势:
- 使用 Category 来提供 Swizzle 实现(如
xxx_method)。 - 在 Category 的
+load中将xxx_method与系统的method交换。 - 不要在 Category 里直接写一个
-(void)method。
这样可以确保:
- 系统原有的逻辑不会丢失。
- 你的 Swizzle 逻辑能准确定位到系统实现。
- 其他开发者(或者你自己)在另一个 Category 里做的 Swizzle 也能形成正确的“指针链”。