消息传递机制:objc_msgSend流程、消息查找、方法缓存、消息转发
聊到 Runtime,绕不开的就是消息传递。很多人觉得 objc_msgSend 就是个黑盒,调个方法就完事了。其实不然,这里面藏着 OC 动态特性的灵魂。
我个人习惯把消息传递拆成三个阶段来看:快速查找、慢速查找、消息转发。说白了,就是先翻缓存,再查类表,最后实在不行就问你能不能救一下。
objc_msgSend 的汇编入口
你可能会问,为什么 objc_msgSend 要用汇编写?嗯,这里有个性能的考量。C 语言没法直接操作寄存器,而消息发送是 OC 里最频繁的操作,每调一个方法都要走一遍。用汇编可以做到极致优化。
核心流程其实就这几步:
- 检查 receiver 是否为 nil
- 从 receiver 的 isa 指针找到类对象
- 在类对象的缓存中查找方法
- 缓存命中直接调用,未命中走慢速查找
关键点:receiver 为 nil 时,objc_msgSend 直接返回,不会 crash。这就是为什么你给 nil 发消息不会崩。
消息查找:从缓存到类表
先说说缓存查找。每个类对象都有一个 cache_t 结构体,里面维护了一个哈希表。key 是 SEL,value 是 IMP。查找时直接用 SEL 哈希取模,时间复杂度 O(1)。
我在项目中遇到过一个问题:某个类的初始化方法里调了大量子方法,结果第一次调用特别慢。后来一查,原来是缓存还没建立,每个方法都要走一遍完整的查找流程。解决方案就是在 +load 或 +initialize 里预热一下。
缓存没命中怎么办?那就走慢速查找。流程是这样的:
- 沿着类的继承链往上找
- 先查当前类的 method_list_t
- 没找到就去父类查
- 一直查到 NSObject 为止
- 找到了就缓存到当前类,然后返回 IMP
- 找不到就进入消息转发
避坑指南:我曾经在分类里重写了系统方法,结果发现有时候生效有时候不生效。后来才意识到,分类的方法会被加载到类的 method_list 里,但如果有两个分类重写了同一个方法,谁先被加载谁就生效。这取决于编译顺序和链接顺序。
方法缓存的结构与优化
缓存不是无限大的。cache_t 的容量是动态扩容的,初始大小是 4,满了就翻倍。当缓存占用超过 3/4 时,会触发清空操作——把整个缓存清掉,重新开始。
你想想看,为什么是清空而不是部分淘汰?因为方法调用的局部性原理:一个类的方法通常集中在某一段时间内被频繁调用。清空后重新缓存,反而更高效。
| 缓存状态 | 行为 | 触发条件 |
|---|---|---|
| 未满 | 直接插入 | 哈希槽位为空 |
| 冲突 | 线性探测 | 槽位被占用 |
| 超过阈值 | 扩容并清空 | 占用 > 3/4 |
消息转发:最后的救命稻草
如果慢速查找也没找到,Runtime 不会直接 crash,而是给你三次机会:
- 动态方法解析:调用 +resolveInstanceMethod: 或 +resolveClassMethod:
- 快速转发:调用 -forwardingTargetForSelector:
- 完整转发:调用 -methodSignatureForSelector: 和 -forwardInvocation:
说白了,第一次问你「能不能动态加个方法」,第二次问你「能不能把这个消息转给别人」,第三次问你「能不能自己处理这个调用」。三次都拒绝了,那就只能 unrecognized selector crash 了。
注意:消息转发是有性能代价的。每次转发都要创建 NSInvocation 对象,涉及内存分配和消息传递。我在线上项目里见过有人滥用 forwardingTargetForSelector 来做 AOP,结果导致卡顿。能用缓存解决的问题,别走转发。
知识体系总览
下面这张图是我梳理的消息传递完整流程,建议你保存下来对照着看:
实际项目中的避坑指南
最后分享几个我踩过的坑:
- KVO 的 isa-swizzling:KVO 会动态创建子类并修改 isa 指针,这时候 objc_msgSend 查找的是子类的方法列表。如果你在 KVO 期间调了 class 方法,返回的是子类而不是原类。我曾经因为这个 bug 排查了两天。
- 消息转发与多线程:消息转发过程中如果涉及多线程,要注意 NSInvocation 的线程安全。我建议在转发方法里加锁,或者用 dispatch_sync 串行化。
- 不要滥用 method_exchangeImplementations:方法交换本质是修改 IMP 指针,但如果你交换的是父类方法,会影响所有子类。我见过有人交换了 NSObject 的 dealloc,结果整个 App 的内存管理都乱了。
嗯,消息传递这块内容确实不少。但只要你理解了缓存、查找、转发这三层结构,遇到任何 OC 的运行时问题都能快速定位。下次遇到 unrecognized selector crash,别急着加方法,先想想是不是消息转发没处理好。
公众号:蓝海资料掘金营,微信deep3321