1-17.【OC】【基础】为什么 Objective-C 的方法调用“看起来慢”?

2 阅读3分钟

Objective-C 的方法调用“看起来慢”,是因为它在底层将函数调用变成了数据库查询

在传统的 C++ 或 Swift 中,调用方法通常是直接跳转到内存地址(静态派发)或查找偏移量固定的虚函数表(V-Table)。而在 Objective-C 中,每次调用方法都要通过 objc_msgSend 走一遍复杂的流程。

以下是导致它“体感偏慢”的四个核心原因:


1. 字符串匹配的开销

在底层,方法的标识符 SEL 本质上是一个字符串映射。

  • C++/Swift:在编译期就确定了函数在内存中的偏移量,CPU 只需要执行一条 jmp(跳转)指令。
  • Objective-C:必须拿着 SEL 这个“名字”去类的哈希表(Cache)或方法列表里进行匹配。即便有哈希加速,也比直接跳转多出了数次内存寻址和对比。

2. 缓存维护的代价

为了解决查找慢的问题,每个类都维护了一个 cache_t。但这个缓存不是免费的:

  • 哈希冲突:如果多个方法映射到同一个缓存槽位,性能会下降。
  • 扩容与清空:当类的方法变多导致缓存装满时,Runtime 会执行扩容操作,并清空旧缓存。这意味着某些调用会偶尔遇到“冷启动”,性能瞬间掉落。

3. 继承链的递归回溯

如果方法在当前类中没找到,Runtime 必须沿着 superclass 指针一级一级往上爬。

  • 如果一个类的继承链很深(例如在复杂的 UI 框架中),而在顶层父类才定义了该方法,那么在“缓存未命中”的情况下,这种递归遍历是非常昂贵的 CPU 操作。

4. 无法进行“编译器优化”

这是对性能影响最大的潜规则。

  • 内联(Inlining)受阻:现代编译器通过将小函数直接“嵌入”调用处来极大提升速度。但由于 Obj-C 的方法实现在运行时是可以被 Method Swizzling 动态替换的,编译器不敢对其进行内联。
  • 流水线预测失败:CPU 喜欢预测代码的下一跳。由于 objc_msgSend 的目标是动态确定的,CPU 的分支预测器(Branch Predictor)更难命中,导致指令流水线经常需要重填。

既然慢,为什么 iOS 依然流畅?

虽然“看起来慢”,但苹果在底层做了极其精密的优化:

  1. 纯汇编实现objc_msgSend 是用手工优化的汇编代码编写的,最大程度减少了寄存器压栈和出栈的开销。
  2. 缓存命中率极高:在实际工程中,99% 的方法调用都能在第一级 Cache 中命中,此时的耗时仅比 C 函数调用多出几个纳秒。
  3. IO 与 渲染瓶颈:相比于屏幕刷新的 16ms 和网络请求的几百毫秒,objc_msgSend 的几十个时钟周期几乎可以忽略不计。

总结

Objective-C 的慢是相对的。它牺牲了极小部分的执行效率,换取了极其强大的运行时灵活性(如 KVO、Core Data、热修复等)。