Method Swizzling 的本质是利用 Objective-C 的动态性,在运行时(Runtime)修改类对象中 Selector(SEL) 与 Implementation(IMP) 的映射关系。
简单一句话总结:它交换了方法列表(Method List)中 struct method_t 结构体里的 imp 指针。
1. 交换的核心:method_t 结构体
在 Objective-C 的类结构中,方法列表存储的是一个个 method_t 结构。要理解交换了什么,必须看它的定义:
C++
struct method_t {
SEL name; // 方法名(如 @selector(viewWillAppear:))
const char *types; // 参数和返回值类型编码
IMP imp; // 函数实现的指针(指向真正的汇编代码地址)
};
当你调用 method_exchangeImplementations(methodA, methodB) 时,Runtime 做的唯一一件事就是:把 methodA 的 imp 指针和 methodB 的 imp 指针做了一个物理上的值交换。
2. 图解交换前后的变化
假设我们要把系统方法 method_A 和我们自定义的方法 method_B 进行交换:
交换前:
SEL_AIMP_A(系统实现)SEL_BIMP_B(自定义实现)
交换后:
SEL_AIMP_BSEL_BIMP_A
此时,当你代码里调用 [obj method_A] 时,objc_msgSend 查找到的 IMP 却是 IMP_B,从而实现了拦截和功能增强。
3. 为什么交换后还要“递归调用”自己?
在 Swizzling 的实现模板中,经常看到这种代码:
Objective-C
- (void)xxx_viewWillAppear:(BOOL)animated {
[self xxx_viewWillAppear:animated]; // 看起来像死循环?
NSLog(@"拦截到了!");
}
底层逻辑:
由于 xxx_viewWillAppear: 的 SEL 现在指向的是原始系统实现(IMP_A),所以这里的 [self xxx_viewWillAppear:animated] 实际上执行的是原生的 viewWillAppear:。这不仅不会死循环,反而是调用原生逻辑的标准做法。
4. 关键的“副作用”:Method Cache 的清理
这是很多人忽略的底层细节:Swizzling 发生后,Cache 会发生什么?
为了保证调用安全,method_exchangeImplementations 在修改完 imp 指针后,会立即调用 flushCaches(cls):
- 它会清空该类及其所有子类的 Method Cache。
- 原因:如果不清空,
objc_msgSend可能还会从缓存里拿到交换前的旧IMP地址,导致逻辑不一致。这也是为什么 Swizzling 之后,第一次方法调用会变慢(重新走慢速路径查找)。
5. 2026 年的安全实践指南
尽管 Swizzling 强大,但在现代开发(尤其是在 iOS 19+ 时代)中,必须遵循以下原则以防崩溃:
- 原子性:必须在
+load方法中执行,确保在类加载时就完成交换。 dispatch_once:虽然+load本身是线程安全的,但为了防止手动调用,通常包裹在dispatch_once中。- 检查类层级:一定要先用
class_addMethod尝试添加。如果子类没实现该方法,直接交换会把“父类”的方法给换掉,导致所有子类都被影响(这通常不是你想要的)。
总结
Method Swizzling 交换的不是 SEL,也不是 Method 结构体本身,而是结构体内部的函数指针 IMP。它通过 Runtime 强行改写了映射表,并配合缓存刷新机制确保修改生效。