3-16.【OC】【Runtime】Swift 的方法调用是否也存在类似 Method Cache 的机制?和 OC Runtime 有哪些差异?

0 阅读3分钟

Swift 确实有缓存优化,但它的核心机制与 Objective-C 的动态哈希表查找完全不同。

Swift 的设计哲学是**“尽可能静态化”**。为了追求极致性能,它将大部分工作从“运行时”挪到了“编译时”。


1. Swift 的三种派发机制 (Dispatch)

要聊 Swift 的缓存,必须先看它的三种派发方式。Swift 没有像 OC 那样“一把梭”全走 objc_msgSend,而是根据场景选择了不同的路径:

派发方式对应场景性能机制
直接派发 (Static)structfinal class、扩展中的方法极快 (类似 C++)编译时确定地址,支持内联 (Inline)
函数表派发 (Table)class 中的普通方法使用 vtablewitness table
消息派发 (Message)带有 @objc dynamic 的方法较慢走 OC Runtime (objc_msgSend)

2. Swift 的“缓存”机制:vtable

在 Swift 类中,类似于 OC Method Cache 的机制是 vtable (Virtual Method Table)

它和 OC Cache 的区别:

  • OC Cache (哈希表) :动态生成的。第一次调用时没有,找完后填进去。它是按需加载的。
  • Swift vtable (数组) :在编译时就固定好了。每一个 class 在内存中都有一个连续的数组,里面存好了所有方法的地址偏移量。

为什么 Swift 理论上更快?

  • OC:需要计算哈希值、处理哈希冲突、从内存读取 cache_t

  • Swift:只需要“基地址 + 偏移量”即可命中。

    • MOV X0, [X1, #0x10] (伪代码:直接从偏移 16 字节处取地址并跳转)。
    • 这种方式对 CPU 的分支预测极其友好。

3. 核心差异对比:Swift vs OC

① 动态性 vs 静态性

  • OC:支持运行时交换方法(Swizzling)。因为它是查哈希表,你换了表里的内容,调用就变了。
  • Swift (vtable) :默认不支持 Swizzling。偏移量在编译后就“写死”在二进制里了。

② 协议的缓存:Witness Table

Swift 的协议(Protocol)使用 PWT (Protocol Witness Table)

当一个泛型函数接收一个协议类型时,Swift 会带上一个隐藏的指针指向 PWT。这比 OC 的 respondsToSelector:performSelector: 要安全且高效得多。

③ 缓存失效

  • OC:扩容、Swizzling 或类结构改变都会导致 cache_t 整个清空。
  • Swift:由于是静态确定的表,不存在缓存失效的情况。

4. 特殊情况:Swift 中的“影子”缓存

虽然 Swift 主打静态,但在以下两种情况它会表现得像 OC:

  1. NSObject 子类:如果你继承自 NSObject 且没有标记 final,Swift 依然可能为了兼容性生成一层包装,在内部调用 objc_msgSend,从而利用 OC 的 Method Cache。
  2. Devirtualization (去虚拟化) :Swift 编译器(SIL 优化器)非常聪明。如果它在编译时发现某个方法虽然写在 class 里但从未被子类重写,它会直接把这个方法“降级”为直接派发,直接消除所有查找开销。

5. 总结:性能的代价

  • Objective-C 牺牲了一点点初始性能,换取了极其强大的灵活性(如运行时热修复、Aspects 等)。
  • Swift 牺牲了灵活性,换取了确定的、接近底层的执行效率

比喻

OC 是一个高级餐厅:你坐下点菜(发送消息),服务员去厨房问(查缓存),如果没有就看菜谱(找方法列表),最后把菜端上来。

Swift 是一个工厂流水线:每个工位(方法)的位置在建厂时就定死了,零件顺着传送带(vtable 偏移量)直接过去。

下一步建议:

如果你在做 Swift 性能优化,最有效的手段是大量使用 finalprivate 关键字。这能直接让编译器放弃 vtable 查找,转而使用直接派发