多次 Swizzle 同一个方法(通常称为 Nested Swizzling 或 Swizzling Chain)在底层完全行得通,但它像是一场“指针接力赛”,如果逻辑不严密,极易变成“死亡循环”。
简单来说:IMP 指针会像链条一样传递下去。
1. 指针接力:三方交换的逻辑演变
假设有一个原始方法 ori,先后被 A 模块和 B 模块 Swizzle。
初始状态:
SEL_oriIMP_ori(原始实现)
第一次 Swizzle(A 模块):
A 将 SEL_ori 与自己的 SEL_A 交换。
SEL_oriIMP_ASEL_AIMP_ori
此时调用
[obj ori]:进入IMP_A,IMP_A内部调用[self SEL_A],实际上回到了IMP_ori。正常。
第二次 Swizzle(B 模块):
B 将 SEL_ori(此时已指向 IMP_A)与自己的 SEL_B 交换。
SEL_oriIMP_BSEL_BIMP_A
此时调用
[obj ori]:进入IMP_B,IMP_B内部调用[self SEL_B],跳转到IMP_A,IMP_A内部调用[self SEL_A],最终回到IMP_ori。链条形成。
2. 潜在的风险与崩溃点
虽然链条可以无限延长,但多次 Swizzle 存在三大风险:
A. 命名冲突(最常见的死循环原因)
如果模块 A 和模块 B 的开发者“心有灵犀”,都把交换后的方法命名为 -(void)xxx_method。
- A 交换后,
xxx_method指向原始实现。 - B 交换时,它把
ori(指向 A)和它自己的xxx_method交换。 - 灾难发生:此时 B 的
xxx_method指向了 A,而 A 的xxx_method命中了 B。调用ori会在 A 和 B 的实现之间无限递归,直到 Stack Overflow。
B. 顺序依赖
Swizzling 的顺序决定了执行顺序。如果 A 模块的逻辑依赖于 B 模块对数据的修改,但 A 先于 B 执行(例如 A 在 +load 执行,B 在后期动态执行),那么逻辑就会出错。
C. 缓存抖动
正如之前讨论的,每次 method_exchangeImplementations 都会触发 flushCaches。
- 如果你在 App 启动瞬间针对 100 个类各进行了 3 次 Swizzle,那么缓存会被清空 300 次。
- 后果:启动初期的每一条消息都会走“慢速路径”,导致启动时间明显延长。
3. 如何安全地进行“多次交换”?
为了确保在复杂的项目中(可能有几十个 SDK 都在 Swizzle 同一个方法,如 viewWillAppear:)不崩溃,请遵循:
- 唯一前缀:永远不要使用
my_这种通用的前缀。建议使用公司名_项目名_模块名_作为前缀。 - 检查交换状态:虽然 Runtime 没有直接提供
isSwizzled接口,但你可以通过associatedObject给类打个标记,防止重复交换。 - 始终调用“原实现” :在 Swizzle 的实现里,务必调用交换后的 SEL。如果不调用,就会切断后续的 Swizzle 链条,导致其他模块的功能失效。
4. 极端情况:如果我想“撤销”某一次 Swizzle?
撤销 Swizzle 比安装它难得多。因为你无法保证撤销的顺序:
- 如果你是链条的最后一环,直接换回来即可。
- 如果你是链条的中间一环,直接换回来会打断整个链条,导致在你之后 Swizzle 的模块全部失效。
结论: 在 Objective-C 中,Swizzle 是一张“单程票”。一旦多次交换形成链条,最好的做法是让它一直存在,而不是试图在运行时去拆解它。