← 返回全部文章

Objective-C Runtime:Method Swizzling 的边界与安全写法

方法交换很有能力,也很容易把全局行为变得不可预测。本文梳理它的运行机制、典型风险,以及确实需要使用时的最低安全边界。

本文目录
  1. 交换发生在哪里
  2. 先判断能不能不用
  3. 处理继承关系
  4. 确保只执行一次
  5. 保持方法签名一致
  6. 测试全局影响

Method Swizzling 会在运行时交换两个 selector 对应的方法实现。它常见于埋点、兼容性修复和框架内部,但它改变的是类的全局行为:同一进程中所有实例都会受到影响。

因此,第一个问题不应该是“怎么交换”,而应该是“这个需求是否真的需要交换”。

交换发生在哪里

Objective-C 消息发送最终会根据类的方法列表找到 IMP。method_exchangeImplementations 交换的是两个 Method 中保存的实现地址,并不会修改调用处的 selector。

Method originalMethod = class_getInstanceMethod(self, @selector(viewDidAppear:));
Method swizzledMethod = class_getInstanceMethod(self, @selector(hn_viewDidAppear:));

method_exchangeImplementations(originalMethod, swizzledMethod);

交换之后,调用 viewDidAppear: 会进入 hn_viewDidAppear: 的实现;而交换后的 hn_viewDidAppear: 会指向原实现。所以自定义实现里看起来像递归的调用,实际是在调用原方法。

先判断能不能不用

以下替代方案通常更清晰:

  • 业务页面行为:使用基类、组合对象或协议;
  • 用户交互埋点:在统一路由、Action 或事件分发层记录;
  • 第三方依赖行为:优先使用公开扩展点或包装器;
  • 仅调试使用:通过调试工具或条件编译实现;
  • 单个对象行为:考虑消息转发或显式代理,而不是修改整个类。

Swizzling 最适合的是“无法控制调用方、必须覆盖全局、又没有公开扩展点”的窄场景。

处理继承关系

直接交换有一个隐藏条件:原方法可能只存在于父类。如果贸然交换,子类的方法列表和父类实现之间会产生不直观的关系。

更稳妥的模板会先尝试向当前类添加方法:

+ (void)hn_swizzleInstanceMethod:(SEL)originalSelector
                     withMethod:(SEL)swizzledSelector {
    Method originalMethod = class_getInstanceMethod(self, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector);

    if (!originalMethod || !swizzledMethod) {
        return;
    }

    BOOL didAddMethod = class_addMethod(
        self,
        originalSelector,
        method_getImplementation(swizzledMethod),
        method_getTypeEncoding(swizzledMethod)
    );

    if (didAddMethod) {
        class_replaceMethod(
            self,
            swizzledSelector,
            method_getImplementation(originalMethod),
            method_getTypeEncoding(originalMethod)
        );
    } else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

当原方法继承自父类时,class_addMethod 会在当前类上添加自定义实现,再把另一个 selector 指回原实现;当原方法已经由当前类实现时,才进行直接交换。

确保只执行一次

交换必须是幂等的。重复执行一次会把实现换回去,再执行一次又会换过去,行为很难排查。

可以在显式入口里用 dispatch_once

+ (void)installAnalyticsHooks {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [UIViewController hn_swizzleInstanceMethod:@selector(viewDidAppear:)
                                        withMethod:@selector(hn_viewDidAppear:)];
    });
}

相较于把逻辑放进 +load,显式安装更容易控制启用条件和初始化顺序。若框架必须在 +load 中处理,也应保持实现极短,并避免依赖其他尚未初始化的模块。

保持方法签名一致

交换的两个方法必须具有兼容签名,包括返回值、参数数量和参数类型。签名不一致可能不会立刻崩溃,但寄存器或栈中的参数会被错误解释。

同时还要留意这些边界:

  1. 多个框架可能交换同一个方法,最终调用链取决于安装顺序;
  2. 系统私有类和私有 selector 不应成为正式功能依赖;
  3. 新系统可能修改内部实现,使原有假设失效;
  4. 交换后的实现要保持原方法的线程和生命周期语义;
  5. 分类中的同名方法本身就存在加载顺序不确定性。

测试全局影响

测试不仅要覆盖“自定义代码被执行”,还要确认:

  • 原实现恰好执行一次;
  • 子类覆盖方法时调用链正确;
  • 同一安装入口重复调用不会改变结果;
  • 功能关闭时不会发生交换;
  • 新旧系统版本上的生命周期顺序没有变化。

Method Swizzling 不是不能用,而是需要把它当作一次全局运行时修改。范围越窄、入口越明确、测试越完整,未来维护时就越少依赖运气。