3-19.【OC】【Runtime】多次 Swizzle 同一个方法会发生什么?

3 阅读3分钟

多次 Swizzle 同一个方法(通常称为 Nested SwizzlingSwizzling Chain)在底层完全行得通,但它像是一场“指针接力赛”,如果逻辑不严密,极易变成“死亡循环”。

简单来说:IMP 指针会像链条一样传递下去。


1. 指针接力:三方交换的逻辑演变

假设有一个原始方法 ori,先后被 A 模块和 B 模块 Swizzle。

初始状态:

  • SEL_ori \rightarrow IMP_ori (原始实现)

第一次 Swizzle(A 模块):

A 将 SEL_ori 与自己的 SEL_A 交换。

  • SEL_ori \rightarrow IMP_A
  • SEL_A \rightarrow IMP_ori

此时调用 [obj ori] :进入 IMP_AIMP_A 内部调用 [self SEL_A],实际上回到了 IMP_ori正常。

第二次 Swizzle(B 模块):

B 将 SEL_ori(此时已指向 IMP_A)与自己的 SEL_B 交换。

  • SEL_ori \rightarrow IMP_B
  • SEL_B \rightarrow IMP_A

此时调用 [obj ori] :进入 IMP_BIMP_B 内部调用 [self SEL_B],跳转到 IMP_AIMP_A 内部调用 [self SEL_A],最终回到 IMP_ori链条形成。


2. 潜在的风险与崩溃点

虽然链条可以无限延长,但多次 Swizzle 存在三大风险:

A. 命名冲突(最常见的死循环原因)

如果模块 A 和模块 B 的开发者“心有灵犀”,都把交换后的方法命名为 -(void)xxx_method

  1. A 交换后,xxx_method 指向原始实现。
  2. B 交换时,它把 ori(指向 A)和它自己的 xxx_method 交换。
  3. 灾难发生:此时 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:)不崩溃,请遵循:

  1. 唯一前缀:永远不要使用 my_ 这种通用的前缀。建议使用 公司名_项目名_模块名_ 作为前缀。
  2. 检查交换状态:虽然 Runtime 没有直接提供 isSwizzled 接口,但你可以通过 associatedObject 给类打个标记,防止重复交换。
  3. 始终调用“原实现” :在 Swizzle 的实现里,务必调用交换后的 SEL。如果不调用,就会切断后续的 Swizzle 链条,导致其他模块的功能失效。

4. 极端情况:如果我想“撤销”某一次 Swizzle?

撤销 Swizzle 比安装它难得多。因为你无法保证撤销的顺序:

  • 如果你是链条的最后一环,直接换回来即可。
  • 如果你是链条的中间一环,直接换回来会打断整个链条,导致在你之后 Swizzle 的模块全部失效。

结论: 在 Objective-C 中,Swizzle 是一张“单程票”。一旦多次交换形成链条,最好的做法是让它一直存在,而不是试图在运行时去拆解它。