4. Method Swizzling:原理与实现、AOP编程、实际应用场景、常见坑点

Method Swizzling,说白了就是「偷梁换柱」。

它利用 Runtime 的动态性,在运行时交换两个方法的实现。你调用的是 A 方法,实际执行的是 B 方法的代码。我第一次接触这个特性时,觉得这简直是黑魔法——太强了,也太危险了。

4.1 原理:为什么能交换?

先回顾一下方法调用的本质。在 Objective-C 里,[obj message] 会被编译成 objc_msgSend(obj, @selector(message))。Runtime 通过 selector 在类的方法列表中查找对应的 IMP(函数指针)。

Method Swizzling 做的就是:把 selectorAIMP 指向 methodB 的实现,同时把 selectorBIMP 指向 methodA 的实现。

核心数据结构:

typedef struct method_t {
    SEL name;          // 方法名
    const char *types; // 类型编码
    IMP imp;           // 函数指针
} Method;

交换的本质,就是交换 imp 指针。

我习惯用一张图来理解这个过程:

Method Swizzling 原理示意图 交换前 @selector(original) @selector(swizzled) IMP_original IMP_swizzled 交换后 @selector(original) @selector(swizzled) IMP_swizzled IMP_original 交换后,调用 original 方法实际执行的是 swizzled 的 IMP Selector IMP(交换前) IMP(交换后)

4.2 标准实现模板

我见过很多新手直接写 method_exchangeImplementations 就完事了。但这样做很容易出问题。下面是我在项目中一直使用的标准模板:

#import <objc/runtime.h>

@implementation UIViewController (Swizzling)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        
        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(xxx_viewWillAppear:);
        
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        
        // 先尝试添加方法
        BOOL didAddMethod = class_addMethod(class,
            originalSelector,
            method_getImplementation(swizzledMethod),
            method_getTypeEncoding(swizzledMethod));
        
        if (didAddMethod) {
            // 添加成功,说明 original 方法不存在
            // 直接替换 swizzled 的实现
            class_replaceMethod(class,
                swizzledSelector,
                method_getImplementation(originalMethod),
                method_getTypeEncoding(originalMethod));
        } else {
            // 添加失败,说明 original 方法已存在
            // 直接交换实现
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

- (void)xxx_viewWillAppear:(BOOL)animated {
    // 这里调用的是原始的 viewWillAppear
    // 因为实现已经被交换了
    [self xxx_viewWillAppear:animated];
    
    // 添加自己的逻辑
    NSLog(@"Swizzled: %@ viewWillAppear", NSStringFromClass([self class]));
}

@end

为什么用 dispatch_once?

Swizzling 只需要执行一次。如果多次执行,会导致交换回原状,甚至崩溃。我见过有人把 swizzling 写在 +initialize 里,结果子类继承时又触发了一次,直接翻车。

4.3 AOP 编程:面向切面编程

Method Swizzling 是实现 AOP 的核心手段。AOP 的思想是:把横切关注点(如日志、统计、权限)从业务逻辑中抽离出来。

举个例子,你想统计所有页面的停留时间。传统做法是在每个 viewWillAppearviewWillDisappear 里加代码。这太蠢了,对吧?

用 AOP 的思路,一次 swizzling 搞定:

@implementation UIViewController (Tracking)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        swizzleMethod([self class], @selector(viewWillAppear:), @selector(track_viewWillAppear:));
        swizzleMethod([self class], @selector(viewWillDisappear:), @selector(track_viewWillDisappear:));
    });
}

- (void)track_viewWillAppear:(BOOL)animated {
    [self track_viewWillAppear:animated];
    [Tracker startTracking:self];
}

- (void)track_viewWillDisappear:(BOOL)animated {
    [self track_viewWillDisappear:animated];
    [Tracker endTracking:self];
}

@end

你看,所有 UIViewController 的子类都自动获得了埋点能力。这就是 AOP 的魅力——非侵入式地增强功能。

4.4 实际应用场景

我整理了几个我在项目中真正用过的场景:

场景 说明 风险等级
无痕埋点 Hook 点击事件、页面生命周期,自动上报
网络请求监控 Swizzle NSURLSession 的请求方法,记录流量
异常防护 Hook NSArray、NSDictionary 的越界方法,防止崩溃
UI 适配 Hook UIFont 的创建方法,全局替换字体大小
性能监控 Hook 图片加载、数据库操作,统计耗时

我个人的经验: 异常防护场景最实用,但也最危险。我曾经在项目中用 swizzling 保护 NSArray 的 objectAtIndex: 方法,结果因为没处理好线程安全,反而引入了更隐蔽的崩溃。嗯,从那以后我加了一层锁。

4.5 常见坑点

这部分我踩过的坑最多,一个一个说。

坑点一:交换顺序导致死循环

很多人以为交换后调用原方法要用 [self originalMethod]。错!因为实现已经被交换了,这样调用会再次进入 swizzled 方法,形成死循环。

正确的做法是调用 swizzled 方法:

// 正确
- (void)xxx_viewWillAppear:(BOOL)animated {
    [self xxx_viewWillAppear:animated]; // 这里调用的是原始实现
    // 自己的逻辑
}

// 错误
- (void)xxx_viewWillAppear:(BOOL)animated {
    [self viewWillAppear:animated]; // 死循环!
}

坑点二:子类继承问题

如果你 swizzle 的是父类的方法,子类没有重写该方法。那么交换后,子类调用该方法会直接走 swizzled 实现。这通常没问题。

但如果你在子类中又 swizzle 了同一个方法,就会产生冲突。我曾经在一个大项目里看到三个不同的 SDK 都 swizzle 了 viewDidAppear:,结果调用顺序完全乱套。

我曾经踩过的坑: 在分类的 +load 方法里 swizzle,但分类的加载顺序是不确定的。如果两个分类都 swizzle 同一个方法,后加载的那个会覆盖前一个。解决方案是:尽量在主类里做 swizzling,或者用 dispatch_once 保证只执行一次。

坑点三:线程安全

method_exchangeImplementations 本身不是线程安全的。虽然在实际开发中,我们通常在 +load 里调用,此时是单线程环境。但如果你在运行时动态 swizzle,就要小心了。

我建议的做法是:

  • 所有 swizzling 操作统一在 +load 中完成
  • 如果必须在运行时 swizzle,加 @synchronized 保护
  • 不要频繁 swizzle,一次到位

坑点四:KVO 冲突

KVO 也会动态修改类的 IMP。如果你 swizzle 了一个被 KVO 观察的属性方法,可能会导致 KVO 通知丢失或重复。

我记得有一次排查一个诡异的 UI 不刷新问题,查了两天才发现是 swizzling 和 KVO 打架了。从那以后,我定了一条规矩:不要 swizzle 被 KVO 观察的属性方法

坑点五:C 函数无法 Swizzle

这个其实不算坑,但很多人会搞混。Method Swizzling 只能交换 Objective-C 方法,不能交换 C 函数。如果你想 hook C 函数,需要用 fishhook 这类工具。

小技巧: 如果你不确定某个方法能不能 swizzle,可以用 class_getInstanceMethod 检查一下。返回 NULL 说明该方法不存在,或者不是 OC 方法。

4.6 最佳实践总结

说了这么多坑,最后给几条我自己的铁律:

  1. 只在 +load 中做 swizzling,别的地方别碰
  2. dispatch_once 保护,确保只执行一次
  3. 调用原方法时,调用 swizzled 版本,别搞混了
  4. 做好命名规范,swizzled 方法加前缀,一眼能看出来
  5. 能不 swizzle 就别 swizzle,能用继承、组合解决的问题,别用黑魔法

Method Swizzling 是一把双刃剑。用好了,它能帮你写出优雅的框架代码;用不好,它会让你在 debug 时痛不欲生。我个人的态度是:敬畏它,但别怕它。理解原理,遵守规范,它就是你工具箱里的一把利器。


公众号:蓝海资料掘金营,微信deep3321