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 做的就是:把 selectorA 的 IMP 指向 methodB 的实现,同时把 selectorB 的 IMP 指向 methodA 的实现。
核心数据结构:
typedef struct method_t {
SEL name; // 方法名
const char *types; // 类型编码
IMP imp; // 函数指针
} Method;
交换的本质,就是交换 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 的思想是:把横切关注点(如日志、统计、权限)从业务逻辑中抽离出来。
举个例子,你想统计所有页面的停留时间。传统做法是在每个 viewWillAppear 和 viewWillDisappear 里加代码。这太蠢了,对吧?
用 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 最佳实践总结
说了这么多坑,最后给几条我自己的铁律:
- 只在
+load中做 swizzling,别的地方别碰 - 用
dispatch_once保护,确保只执行一次 - 调用原方法时,调用 swizzled 版本,别搞混了
- 做好命名规范,swizzled 方法加前缀,一眼能看出来
- 能不 swizzle 就别 swizzle,能用继承、组合解决的问题,别用黑魔法
Method Swizzling 是一把双刃剑。用好了,它能帮你写出优雅的框架代码;用不好,它会让你在 debug 时痛不欲生。我个人的态度是:敬畏它,但别怕它。理解原理,遵守规范,它就是你工具箱里的一把利器。
公众号:蓝海资料掘金营,微信deep3321