文章目录
-
- [1. 核心原理:指针交换](#1. 核心原理:指针交换)
- [2. 错误示范 (The Naive Way)](#2. 错误示范 (The Naive Way))
- [3. 正确姿势 (The Best Practice)](#3. 正确姿势 (The Best Practice))
- [4. 滥用的风险与大坑](#4. 滥用的风险与大坑)
- [5. 什么时候该用,什么时候不该用?](#5. 什么时候该用,什么时候不该用?)
-
- [✅ 推荐场景](#✅ 推荐场景)
- [❌ 拒绝场景](#❌ 拒绝场景)
- 总结
今天,我们聊聊 Runtime 中最著名、也是被误用最多的"黑魔法"------ Method Swizzling(方法交换) 。
虽然 Swift 已经成为主流,但在处理遗留代码、无侵入埋点、APM 监控等场景下,Method Swizzling 依然是不可替代的利器。但由于它的全局副作用,稍有不慎就会引发难以调试的 Bug。
本文将带你掌握 Swizzling 的标准范式 ,并揭示其背后的隐形陷阱。
1. 核心原理:指针交换
在 Objective-C 中,Method(方法) 是一个结构体,本质上维护了一个 SEL(方法名)到 IMP(函数指针)的映射。
c
struct objc_method {
SEL method_name; // 选择子 (Key)
char *method_types; // 类型编码
IMP method_imp; // 函数指针 (Value)
}
Method Swizzling 的本质 :就是在运行时,修改这个结构体,让 SEL A 指向 IMP B,让 SEL B 指向 IMP A。
原理图解
After Swizzling Before Swizzling IMP: Custom Code SEL: viewWillAppear: IMP: Original Code SEL: xxx_viewWillAppear: IMP: Original Code SEL: viewWillAppear: IMP: Custom Code SEL: xxx_viewWillAppear:
这样一来,当你调用系统原生的 viewWillAppear: 时,实际执行的是你写的 Custom Code。
2. 错误示范 (The Naive Way)
很多新手在网上一搜,直接在 +load 方法里写下了这段代码:
objective-c
// ❌ 危险的写法
+ (void)load {
Method original = class_getInstanceMethod(self, @selector(viewWillAppear:));
Method swizzled = class_getInstanceMethod(self, @selector(hook_viewWillAppear:));
method_exchangeImplementations(original, swizzled);
}
这段代码有什么问题?
如果当前类(比如 MyViewController)没有实现 viewWillAppear:,而是继承自父类(UIViewController),那么 class_getInstanceMethod 返回的是父类 的 Method。
直接交换,会导致所有继承自 UIViewController 的类都受到影响,甚至在调用父类方法时崩溃。这被称为**"继承污染"**。
3. 正确姿势 (The Best Practice)
为了避免"继承污染"并保证线程安全,业界公认的"最佳实践"包含三个关键点:
- Dispatch Once:保证只交换一次。
- 先尝试添加 (class_addMethod):检查当前类是否有该方法。
- 再进行交换 (method_exchangeImplementations)。
标准代码模板
objective-c
#import "UIViewController+Tracking.h"
#import <objc/runtime.h>
@implementation UIViewController (Tracking)
+ (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);
// 关键步骤 1: 尝试向当前类添加原方法的实现
// 如果当前类没有实现该方法(是继承来的),class_addMethod 会成功
// 并把 method_getImplementation(swizzledMethod) 作为实现添加进去
BOOL didAddMethod = class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
// 关键步骤 2: 如果添加成功,说明原原本本就没有这个方法
// 我们已经把 originalSelector 指向了 swizzledIMP
// 现在只需要把 swizzledSelector 指向 originalIMP 即可
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
// 关键步骤 3: 如果添加失败,说明当前类本身就实现了这个方法
// 直接交换即可
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
#pragma mark - Method Swizzling
- (void)xxx_viewWillAppear:(BOOL)animated {
// 插入埋点代码
NSLog(@"[Analytics] viewWillAppear: %@", self);
// ⚠️ 注意:这里调用 xxx_viewWillAppear,实际上是调用原始的 viewWillAppear
// 这看起来像死循环递归,其实不是!
[self xxx_viewWillAppear:animated];
}
@end
4. 滥用的风险与大坑
Method Swizzling 就像手术刀,用好了救人,用不好致命。
风险一:方法命名冲突 (Naming Conflict)
如果在 Category 中定义的方法名太普通(例如 swizzled_viewWillAppear),而另一个第三方库也叫这个名字,程序启动时会发生符号覆盖,或者两个库互相 Swizzle,导致逻辑错乱。
- 对策 :务必使用带有项目前缀的命名,如
xxx_viewWillAppear:。
风险二:改变了方法参数 (Args Alteration)
Swizzled 方法的参数列表必须和原方法完全一致 。如果你试图在 Swizzled 方法里少传一个参数或改变参数类型,会导致栈数据错乱,引发 EXC_BAD_ACCESS。
风险三:调用顺序混乱
如果有多个 Category 都 Swizzle 了同一个方法,执行顺序取决于编译链接的顺序(Build Phases -> Compile Sources 中的顺序)。这使得 Bug 极难复现和调试。
风险四:很难调试 (Debug Hell)
当 Swizzling 发生 Crash 时,堆栈里显示的函数名可能和你预期的完全不一样(因为 IMP 已经被换了)。这会让接手你代码的同事想"杀人"。
5. 什么时候该用,什么时候不该用?
✅ 推荐场景
- 无侵入埋点 (AOP):统一统计所有 VC 的页面进入次数、点击事件。
- 全局UI调整 :例如全局修改导航栏字体、颜色,Hook
viewDidLoad。 - 热修复 (Hotfix):线上出现严重 Bug,通过下发脚本动态替换某个方法的实现(这也是 JSPatch 的原理)。
- 解决系统 Bug:比如处理某些 iOS 版本下的键盘弹出崩溃。
❌ 拒绝场景
- 业务逻辑:千万不要在 Swizzling 里写具体的业务逻辑!
- 修改返回值:尽量避免修改原方法的返回值,除非你非常清楚后果。
- SDK 开发:如果你在开发一个 SDK,尽量避免 Swizzle 用户的代码,除非这是你 SDK 的核心功能,并且你对此负责。
总结
Method Swizzling 是 Objective-C Runtime 赋予我们的强大能力。
- 初级开发者 只会
method_exchangeImplementations。 - 高级开发者 懂得使用
class_addMethod防御继承陷阱,懂得使用dispatch_once保证线程安全,更懂得在什么时候克制不使用它。
记住:Magic comes with a price.