Method Cache(方法缓存) 的作用可以用四个字概括:变慢为快。
如果没有缓存,Objective-C 每一行 [obj message] 都要经历昂贵的方法列表遍历和继承链递归查找。而 Method Cache 存在的唯一目的,就是为了让这种动态的消息机制在性能上无限逼近 C 语言的函数调用。
1. 核心作用:消除重复查找的开销
在实际开发中,某个方法一旦被调用,很大记录会在短时间内被再次调用(例如在 for 循环里或 drawRect: 这种高频回调中)。
- 无缓存时:每次调用都要遍历一次方法列表(Method List)。这是一个线性搜索(),如果类的方法很多,或者在父类里,耗时会线性增加。
- 有缓存时:Runtime 会将查找结果(
SEL和IMP)存入一个哈希表。下次调用时,通过哈希算法直接定位()。
2. Method Cache 的物理结构
在 objc_class 结构体中,cache 成员是一个 cache_t 结构,它本质上包含:
- Buckets(桶) :一个数组,存储
struct bucket_t(包含SEL和IMP)。 - Mask(掩码) :用于哈希运算,快速定位数组下标。
- Occupied(已占用量) :记录当前缓存了多少个方法。
3. 它是如何运作的?
- 哈希映射:当调用
objc_msgSend时,取SEL的地址与mask进行&(位与)运算,得到一个起始下标。 - 碰撞处理:如果该位置已经存了别的方法(哈希冲突),则采用开放寻址法,下标减 1 往回找,直到找到匹配的
SEL或一个空槽位。 - 直接跳转:一旦匹配,直接拿到
IMP指针并执行。
4. 性能优化中的“副作用”:缓存扩容与清空
为了平衡内存开销和查找速度,缓存并不是无限大的:
-
动态扩容:当缓存占用率达到临界点(通常是 3/4)时,Runtime 会将缓存空间翻倍(例如从 4 个槽位扩充到 8 个)。
-
全量清空(Flush) :这是最关键的一点。 当缓存扩容时,Runtime 为了保证线程安全和哈希分布的正确性,会清空旧的所有缓存。
后果:你会发现程序运行中,某些原本已经变快的方法会突然产生一次微小的卡顿,因为它们必须重新经历一次“慢速路径”查找并再次填入新缓存。
5. 开发者视角的意义
- Method Swizzling 的底层联动:当你交换两个方法的实现(Swizzling)时,Runtime 会强制清空对应类的缓存。如果不清空,程序可能还在运行旧的缓存代码,导致 Hook 失败。
- Category 的覆盖:由于 Category 的方法会在运行时被插入到方法列表的前面,一旦被调用,它就会率先进入 Cache,从而“遮盖”掉类原本的实现。
总结
Method Cache 是 Objective-C 高效运行的“护城河”。它利用了计算机科学中经典的空间换时间策略,将原本复杂的继承链搜索简化成了近乎瞬时的内存取值。