Cache 是属于“类对象”的私有资产,而不是全局共享的。
Runtime 区分它们并不是靠某个特殊的 ID,而是靠存储的位置。
1. “私有笔记本”原则
在 Objective-C 中,每一个类(Subclass)和它的父类(Superclass)在内存中都是独立的类对象(Class Object) 。
- 每个类对象都有自己独立的
cache_t结构。 - 你可以把
cache想象成每个类随身携带的一个私有笔记本。
2. 查找逻辑:谁收到了消息,就先看谁的笔记本
当你调用 [subObject method] 时,objc_msgSend 的逻辑如下:
- 确定 Receiver 的类:发现
subObject的类是Subclass。 - 查找 Subclass 的 Cache:只会在
Subclass的私有笔记本里找。 - 如果命中:直接执行。
关键点来了:
- 如果子类重写(Override)了方法:慢速路径查找时会先在子类的方法列表里找到子类的
IMP,然后存入子类的 Cache。 - 如果子类没重写:慢速路径会顺着父类链找,最终在父类里找到
IMP,但 Runtime 依然会把这个父类的IMP存入子类的 Cache。
结论:Cache 永远是为“接收者(Receiver)”服务的。即便方法是在父类实现的,一旦被子类调用,它也会在子类的 Cache 里存一份副本,以便下次调用时不用再去爬父类链。
3. 如果两个类都调用了同一个父类方法?
假设 SubclassA 和 SubclassB 都继承自 BaseClass,且都没有重写 init 方法:
- 当你调用
[[SubclassA alloc] init]时,BaseClass的init地址会被缓存在SubclassA的 Cache 中。 - 当你调用
[[SubclassB alloc] init]时,BaseClass的init地址也会被缓存在SubclassB的 Cache 中。
它们互不干扰,物理隔离。
4. 深度细节:Cache 冲突与重写
如果一个方法在子类和父类中都存在,Cache 的内容取决于消息发送的目标。
| 调用场景 | 查找位置 | 缓存结果 (IMP) |
|---|---|---|
[SubObject hello] (子类重写) | Subclass 的 Cache | 指向 子类实现 |
[SuperObject hello] | Superclass 的 Cache | 指向 父类实现 |
[SubObject hello] (子类未重写) | Subclass 的 Cache | 指向 父类实现 |
5. 为什么不合并 Cache 以节省内存?
你可能会想,如果大家都用父类的方法,为什么要存这么多份?
- 性能极速化:如果去查共享 Cache,可能需要加锁或处理复杂的命名空间,而
objc_msgSend追求的是 0.1 纳秒级的响应。 - 动态性:OC 支持在运行时动态修改某个类的方法(Swizzling)。如果 Cache 是共享的,修改一个子类的方法可能会意外影响到父类或其他子类,这会造成灾难性的 Bug。
总结
Cache 区分它们的方法非常简单粗暴:每个类一个坑。
调用者是谁,就去谁的坑里找;找到了就用,找不到就去父类搜,搜到了再回填到自己的坑里。