Method Swizzling 原理 & 风险
适合 iOS / Objective-C Runtime 学习、面试复习、博客发布
一、什么是 Method Swizzling
Method Swizzling 是 Objective-C Runtime 提供的一种能力:
在程序运行时,交换两个方法的实现(IMP) ,从而改变方法调用的实际行为,而不修改原始代码。
一句话概括:
selector 不变,IMP 被替换。
二、Objective-C 方法调用回顾
2.1 表面语法
[obj foo];
2.2 底层真实调用
objc_msgSend(obj, @selector(foo));
2.3 Runtime 查找流程(简化)
-
通过 obj->isa 找到类对象
-
在方法缓存(cache)中查找 selector
-
缓存未命中 → 遍历方法列表
-
找到 selector 对应的 IMP
-
跳转执行该函数
📌 关键点:
Runtime 中 SEL → IMP 的映射是可变的,这正是 Swizzling 能生效的基础。
三、Method 的底层结构
简化后的 Runtime 结构:
typedef struct method_t {
SEL name; // 方法名
const char *types; // 类型编码
IMP imp; // 函数指针
} method_t;
Swizzling 的本质
交换两个 method_t 中的 imp 指针
四、Method Swizzling 的核心 API
4.1 获取 Method
Method m1 = class_getInstanceMethod(cls, @selector(foo));
Method m2 = class_getInstanceMethod(cls, @selector(my_foo));
4.2 交换实现
method_exchangeImplementations(m1, m2);
4.3 交换后的效果
| Selector | 实际执行的 IMP |
|---|---|
| foo | my_foo 的实现 |
| my_foo | foo 的实现 |
五、为什么 Swizzling 后调用“自己”不会死循环
示例代码:
- (void)my_foo {
NSLog(@"before");
[self my_foo];
NSLog(@"after");
}
表面看起来是递归,但实际上:
-
my_foo selector → 指向原始 foo 的 IMP
-
所以这是一次对“原方法”的调用
📌 记忆口诀:
Swizzling 后:名字是假的,IMP 才是真的
六、标准、安全的 Swizzling 写法
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class cls = [self class];
Method original = class_getInstanceMethod(cls, @selector(viewDidAppear:));
Method swizzled = class_getInstanceMethod(cls, @selector(xxx_viewDidAppear:));
method_exchangeImplementations(original, swizzled);
});
}
- (void)xxx_viewDidAppear:(BOOL)animated {
NSLog(@"统计埋点");
[self xxx_viewDidAppear:animated]; // 实际调用原实现
}
为什么必须写在
+load
- 类加载即执行
- 线程安全
- 保证在任何方法调用前完成替换
七、Method Swizzling 的主要风险
7.1 全局生效(不可控)
[UIViewController viewDidAppear:]
一旦 Swizzle:
-
所有 UIViewController 子类
-
系统 + 业务 + 第三方 SDK
全部受影响。
7.2 多方 Swizzling 冲突(最致命)
场景:
-
SDK A swizzle viewDidAppear:
-
SDK B 也 swizzle viewDidAppear:
结果取决于 加载顺序:
-
后者可能覆盖前者
-
调用链断裂
-
死循环 / 逻辑丢失
📌 Swizzling 没有命名空间,也没有优先级控制。
7.3 系统实现变化风险
Apple 不保证系统方法的内部实现稳定。
swizzle dealloc / viewWillDisappear:
iOS 升级后可能导致:
- EXC_BAD_ACCESS
- 野指针
- 不可预期行为
7.4 破坏继承体系
@interface A : NSObject
- (void)foo;
@end
@interface B : A
@end
Swizzle A.foo:
- B 也被强制修改
- 无法只影响父类
7.5 调用时机 & 线程安全问题
❌ 错误示例:
-
在 +initialize
-
多线程中动态 Swizzle
可能导致:
- 方法缓存未刷新
- IMP 状态不一致
7.6 可维护性差
- 调用栈不真实
- 行为被“暗改”
- 新成员极难理解
- Debug 成本极高
八、哪些场景可以使用 Swizzling(慎用)
| 场景 | 说明 |
|---|---|
| 统计埋点 | 页面曝光、点击统计 |
| Crash 防护 | 数组越界、空指针保护 |
| Debug 工具 | 调试、日志注入 |
| SDK / 基础设施 | 能完全控制使用环境 |
九、不推荐使用的场景
🚫 核心业务逻辑
🚫 安全 / 支付相关
🚫 强依赖系统行为
🚫 多 SDK 混合项目
十、比 Swizzling 更安全的替代方案
10.1 继承 + Override(首选)
@interface BaseViewController : UIViewController
@end
10.2 Protocol + 组合
@protocol Trackable
- (void)track;
@end
10.3 消息转发机制
forwardingTargetForSelector:
forwardInvocation:
- 可控
- 可局部生效
- 更符合 Runtime 设计哲学
十一、总结一句话
Method Swizzling 是通过运行时交换方法 IMP 来改变行为的技术,它强大但危险,具有全局生效、顺序依赖和高维护成本的特点,只适合框架级或工具级场景,业务代码中应尽量避免。
十二、延伸阅读
- objc_msgSend 汇编实现
- 方法缓存(cache_t)
- Aspects 原理分析
- Swift 中的 Method Swizzling 限制