在 Objective-C 的 Runtime 源码(以 objc4 为例)中,Method Cache(即 cache_t)本质上是一个哈希表。
简单直接的答案是:Method Cache 的 Key 是 SEL (Selector)。
但在底层实现中,为了追求极致的性能,这个 Key 的处理和存储有一些细节值得挖掘:
1. Key 的本质:SEL
在 bucket_t(缓存桶)的结构定义中,Key 被明确定义为 SEL:
C++
struct bucket_t {
private:
// 在不同的架构下(如 x86_64 或 ARM64),sel 和 imp 的顺序或存储方式可能不同
explicit_atomic<SEL> _sel;
explicit_atomic<uintptr_t> _imp;
};
SEL是什么? 它是一个指向方法名字符串的映射指针(在 64 位系统下,本质上是一个uintptr_t类型的地址)。- 为什么用
SEL? 因为在一次 App 运行生命周期内,相同名字的方法对应的SEL地址是唯一的。直接比较指针地址比比较字符串快得多。
2. 哈希函数:从 SEL 到 Index
当你调用一个方法(例如 [obj message])时,Runtime 并不会遍历整个缓存表。它会通过一个哈希算法快速定位索引:
计算公式:
这里的 mask 是当前缓存表容量减 1(即 capacity - 1)。由于哈希表的大小始终是 2 的幂次方,这个位与运算能确保计算出的索引永远不会越界,且分布相对均匀。
3. 底层存储的演进 (ARM64 优化)
在现代 iOS 设备(ARM64 架构)上,为了节省空间和提升读取效率,Apple 对 cache_t 做了很多“黑科技”优化:
- 编码存储:在某些版本的 Runtime 中,
SEL并不总是以原始指针形式存储,而是可能与mask或其他元数据进行位运算后存储(例如pstrapped指针)。 - 原子性:为了保证多线程安全(比如一个线程在读缓存,另一个线程在扩容重写缓存),Key 的读取和写入使用的是
memory_order_relaxed等原子操作。
4. 如果发生哈希冲突怎么办?
如果两个不同的 SEL 经过 & mask 计算出了同一个 index,Runtime 使用的是 线性探测法(Linear Probing) :
- 如果该位置已经被占用了,且 Key 不匹配。
- 索引减 1(
index--),去前一个位置看。 - 如果减到 0 还没找到,就跳到表的末尾(
index = mask)继续往前找。 - 直到找到匹配的 Key(缓存命中)或者遇到空的槽位(缓存缺失)。
总结
Method Cache 的 Key 就是方法的 Selector (SEL) 。
- 查找时:通过
SEL & mask算出位置。 - 存储时:将
SEL和对应的函数指针IMP一起存入bucket_t。
这种设计配合汇编实现的 objc_msgSend,使得 Objective-C 的方法调用在绝大多数情况下只需要几次简单的位运算和内存寻址就能完成。