消息传递机制:objc_msgSend流程、消息查找、方法缓存、消息转发

聊到 Runtime,绕不开的就是消息传递。很多人觉得 objc_msgSend 就是个黑盒,调个方法就完事了。其实不然,这里面藏着 OC 动态特性的灵魂。

我个人习惯把消息传递拆成三个阶段来看:快速查找慢速查找消息转发。说白了,就是先翻缓存,再查类表,最后实在不行就问你能不能救一下。

objc_msgSend 的汇编入口

你可能会问,为什么 objc_msgSend 要用汇编写?嗯,这里有个性能的考量。C 语言没法直接操作寄存器,而消息发送是 OC 里最频繁的操作,每调一个方法都要走一遍。用汇编可以做到极致优化。

核心流程其实就这几步:

  1. 检查 receiver 是否为 nil
  2. 从 receiver 的 isa 指针找到类对象
  3. 在类对象的缓存中查找方法
  4. 缓存命中直接调用,未命中走慢速查找

关键点: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,而是给你三次机会:

  1. 动态方法解析:调用 +resolveInstanceMethod: 或 +resolveClassMethod:
  2. 快速转发:调用 -forwardingTargetForSelector:
  3. 完整转发:调用 -methodSignatureForSelector: 和 -forwardInvocation:

说白了,第一次问你「能不能动态加个方法」,第二次问你「能不能把这个消息转给别人」,第三次问你「能不能自己处理这个调用」。三次都拒绝了,那就只能 unrecognized selector crash 了。

注意:消息转发是有性能代价的。每次转发都要创建 NSInvocation 对象,涉及内存分配和消息传递。我在线上项目里见过有人滥用 forwardingTargetForSelector 来做 AOP,结果导致卡顿。能用缓存解决的问题,别走转发。

知识体系总览

下面这张图是我梳理的消息传递完整流程,建议你保存下来对照着看:

消息传递完整流程图 objc_msgSend(receiver, sel) receiver == nil ? 直接返回 缓存查找 (cache_t 哈希表) key: SEL → value: IMP ✅ 缓存命中 → 直接调用IMP ❌ 缓存未命中 → 慢速查找 慢速查找:沿继承链遍历 当前类 → 父类 → ... → NSObject 找到 → 缓存到当前类 + 调用 消息转发 (三次机会) 动态解析 → 快速转发 → 完整转发 unrecognized selector 图例 入口/检查 成功路径 查找过程 转发/异常

实际项目中的避坑指南

最后分享几个我踩过的坑:

  • KVO 的 isa-swizzling:KVO 会动态创建子类并修改 isa 指针,这时候 objc_msgSend 查找的是子类的方法列表。如果你在 KVO 期间调了 class 方法,返回的是子类而不是原类。我曾经因为这个 bug 排查了两天。
  • 消息转发与多线程:消息转发过程中如果涉及多线程,要注意 NSInvocation 的线程安全。我建议在转发方法里加锁,或者用 dispatch_sync 串行化。
  • 不要滥用 method_exchangeImplementations:方法交换本质是修改 IMP 指针,但如果你交换的是父类方法,会影响所有子类。我见过有人交换了 NSObject 的 dealloc,结果整个 App 的内存管理都乱了。

嗯,消息传递这块内容确实不少。但只要你理解了缓存、查找、转发这三层结构,遇到任何 OC 的运行时问题都能快速定位。下次遇到 unrecognized selector crash,别急着加方法,先想想是不是消息转发没处理好。


公众号:蓝海资料掘金营,微信deep3321