如果每次 Cache Miss(缓存未命中)都去老老实实地遍历几千个方法的列表,Objective-C 的性能早就崩了。
在进入最慢的“方法列表遍历”之前,Runtime 实际上设计了好几层“减速带”和“快捷通道”。
1. 第一道防线:空指针与 Tagged Pointer
在查找 Cache 之前,objc_msgSend 的汇编代码会先进行一次极速预检:
- Nil Check:如果
receiver是nil,直接清理寄存器并退出(这就是为什么在 OC 里给nil发消息不崩溃)。 - Tagged Pointer:如果是
NSNumber、NSDate等小对象,它们的类信息不在普通的isa里,而是在指针本身。Runtime 会直接跳转到专门为这些小对象优化的查找逻辑,速度极快。
2. 第二道防线:共享缓存 (dyld Shared Cache)
这是 2026 年甚至更早版本 iOS 系统性能的核心秘密。
对于像 NSString、NSArray 这种系统级的类,它们的方法实在是太常用了。Apple 在系统镜像打包时,会将这些类的方法信息预先处理好,放在 dyld 共享缓存 中。
- 优化逻辑:当你的
CustomString继承自NSString时,如果你调用一个NSString的方法,即使子类缓存没中,Runtime 有可能直接去共享区域读取,而不需要去扫描内存中的类结构。
3. 第三道防线:查找时的“二分搜索” (Binary Search)
即便真的需要去方法列表里找,Runtime 也不是每次都从头往后数。
- 排序列表:在编译阶段,类的方法列表通常是按 Selector 的地址排好序的。
- 算法优化:
lookUpImpOrForward在查找时,如果检测到方法列表是有序的,会直接执行 二分查找。对于拥有几百个方法的类,这能将复杂度从 降低到 。
4. 2026 年新特性:Small Method List 优化
在近年的 Runtime 更新中,Apple 引入了 Small Method 概念。
- 原理:以前的方法列表存储的是 64 位的绝对指针(很占空间且读取慢)。现在的“Small Method”使用 32 位相对偏移量。
- 好处:这不仅减小了二进制文件的体积,还显著提高了 CPU 缓存命中率(L1/L2 Cache),因为更多的方法定义被挤在同一个内存页里,CPU 读取一次内存能拿到更多方法信息。
5. 最终路径:消息转发的缓存
如果查完了方法列表还是没找到,系统会进入动态决议(resolveInstanceMethod:)。
有趣的是,动态决议的结果也会被缓存。一旦你动态添加了方法,下次调用就会直接命中 cache_t,永远不会再走一遍“Miss -> 遍历 -> 决议”的老路。
总结:查找路径的优先级
| 阶段 | 路径名称 | 性能开销 | 核心逻辑 |
|---|---|---|---|
| 0 | Pre-flight | 极低 | 处理 nil 和 Tagged Pointer |
| 1 | Fast Path | 极低 | objc_msgSend 汇编查 cache_t |
| 2 | Shared Path | 低 | dyld 共享缓存命中 |
| 3 | Medium Path | 中 | 有序列表的 二分查找 |
| 4 | Slow Path | 高 | 无序列表的 线性遍历 |
| 5 | Dynamic Path | 极高 | 触发动态补救和消息转发 |
一个硬核的小细节
在移动端,内存读取(I/O)往往比 CPU 计算更贵。所以 Runtime 的逻辑是:宁愿多花几个 CPU 周期做位运算和二分查找,也要极力避免去翻动那些还没加载进内存页的方法列表。