3-17.【OC】【Runtime】Method Swizzling 的底层原理是什么?到底交换了什么?

0 阅读3分钟

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 做的唯一一件事就是:methodAimp 指针和 methodBimp 指针做了一个物理上的值交换。


2. 图解交换前后的变化

假设我们要把系统方法 method_A 和我们自定义的方法 method_B 进行交换:

交换前:

  • SEL_A \rightarrow IMP_A (系统实现)
  • SEL_B \rightarrow IMP_B (自定义实现)

交换后:

  • SEL_A \rightarrow IMP_B
  • SEL_B \rightarrow IMP_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+ 时代)中,必须遵循以下原则以防崩溃:

  1. 原子性:必须在 +load 方法中执行,确保在类加载时就完成交换。
  2. dispatch_once:虽然 +load 本身是线程安全的,但为了防止手动调用,通常包裹在 dispatch_once 中。
  3. 检查类层级:一定要先用 class_addMethod 尝试添加。如果子类没实现该方法,直接交换会把“父类”的方法给换掉,导致所有子类都被影响(这通常不是你想要的)。

总结

Method Swizzling 交换的不是 SEL,也不是 Method 结构体本身,而是结构体内部的函数指针 IMP。它通过 Runtime 强行改写了映射表,并配合缓存刷新机制确保修改生效。