Runtime 与消息传递机制

Objective-C 是一门动态语言,其核心在于 Runtime 系统。消息传递(Message Passing)的底层依赖 isa 指针、方法缓存 和 消息转发 三大机制。掌握这些知识点既能应对高频面试题,也能指导你在实际项目中利用 Runtime 实现动态特性、AOP 和热修复。

消息发送 查找缓存 命中→直接调用 未命中 遍历方法列表 找到→缓存+调用 未找到 消息转发(3阶段) 💡 isa指针确定类对象,方法缓存以哈希表存储SEL→IMP,消息转发是兜底机制
📌 核心考点图谱: isa指针 → 方法缓存 → 消息转发,三者层层递进。面试官通常会从objc_msgSend源码执行路径切入,考察你对整个动态链的掌握。

面试问答卡片

🎯 问题1:请解释 isa 指针的作用,以及在 Runtime 中如何通过它判断对象类型?

考察点是否理解实例对象的 isa 指向类对象、类对象的 isa 指向元类;是否了解 object_getClass() 与 class 方法的区别;是否熟悉继承链的查找过程
回答框架1. isa 是实例对象到类对象的关键指针;2. 通过 isa 可以获取对象的类元数据;3. 通过 superclass 遍历继承链;4. 实际应用中用于类型判断、KVO 实现、消息发送起点
示例回答isa 是 instance 指向 class 的指针,class 的 isa 指向 meta-class。发送消息时 objc_msgSend 通过 isa 找到类,再查找方法缓存。我们可以用 object_getClass(obj) 获取实际类,利用 isa 的掩码(arm64)获取真实地址。
可能追问non-pointer isa 做了什么优化?如何判断一个对象是某个类的实例?(答:isKindOfClass 遍历 superclass)isa 的位域如何保存引用计数?
避坑提醒❌ 不要直接说 isa 指向类对象就够了,要强调在 arm64 下 isa 并非纯指针,需要 & ISA_MASK 才能得到类地址。同时区分 class 方法返回的是类对象,而 object_getClass 返回的是 isa 指向的对象(可能是元类)。
消息转发三阶段追问路径 ① resolveInstanceMethod 追问:如何动态添加方法? 陷阱:返回 YES 后仍会走转发? ② forwardingTarget 追问:返回 nil 会怎样? 应用:实现多继承、代理转发 ③ forwardInvocation 追问:methodSignature 如何生成? 完整转发的性能损耗? 面试官常沿着这个路径挖到「如何处理异常」和「AOP 实践」

🎯 问题2:Runtime 中的方法缓存(Method Cache)是如何设计的?如何查找和更新?

考察点是否理解 cache_t 的结构(哈希表+桶)、缓存查找算法(开放寻址法)、缓存扩容时机、缓存与类继承关系
回答框架1. objc_class 中有 cache_t cache;2. key 是 SEL(方法选择器),value 是 bucket_t(IMP+方法名);3. 查找:objc_msgSend 先查本类缓存,命中直接调用;4. 未命中则遍历方法列表,找到后缓存到本类;5. 扩容时重新哈希
示例回答每个类拥有独立的 cache_t,采用哈希表存储。查找时用 SEL 取模得到索引,如果冲突则线性探测。当缓存占用超过 3/4 时会两倍扩容。通过缓存大幅提升热点方法调用性能,这也是消息转发前最快的路径。
可能追问为什么使用开放寻址法而不是挂载链表?缓存中的方法是否包含父类方法?什么时候缓存会被清空?(答:类添加分类时 flush cache)
避坑提醒❌ 常见错误答案:缓存全局的 / 缓存了父类的方法。实际上缓存只存本类的方法,但查找时如果本类未命中,会沿着 superclass 指针去父类的缓存中继续查找(父类方法不会缓存到子类中)。另外注意 _cmd 是 SEL,不是 IMP。

🎯 问题3:当一个对象无法响应某个方法时,Runtime 的消息转发机制是如何工作的?你会如何利用它实现动态方法解析或代理?

考察点三个阶段的调用顺序、每个阶段的责任、如何终止转发、实际应用场景(如多继承、AOP、热修复)
回答框架1. 先进入 +resolveInstanceMethod: 动态添加方法;2. 未处理则进入 -forwardingTargetForSelector: 转发给其他对象;3. 若返回 nil 则进入完整转发:methodSignatureForSelector: → forwardInvocation:
示例回答当找不到 IMP 时,Runtime 首先调用 resolveInstanceMethod 允许开发者动态添加方法,比如 @dynamic 属性。若未添加,则调用 forwardingTargetForSelector 转给另一个对象,实现类似多继承的效果。最后通过 forwardInvocation 将消息封装成 NSInvocation 处理,常用于 Unrecognized Selector 异常防护。
可能追问如何避免无限递归?forwardingTarget 和 forwardInvocation 分别适合什么场景?消息转发对性能有多大影响?(答:增加消息发送链路,一般仅用于非频繁调用路径)
避坑提醒❌ 混淆阶段:forwardingTargetForSelector 返回 nil 才会进入完整转发,而不是返回 self。在 resolveInstanceMethod 中必须使用 class_addMethod 并返回 YES,否则会继续走转发。完整转发时一定要实现 methodSignatureForSelector 并返回合法签名,否则会 crash。
💡 面试加分项:能结合源码版本(objc4-818.2)描述 objc_msgSend 的汇编实现、cache_t 中的 _bucketsAndMaybeMask 的联合体结构、preopt_cache 等优化细节。面试官会对你“看过源码”印象深刻。
⚠️ 高频易错:消息转发三个阶段并不是每次都会全部调用——第二阶段返回非 nil 对象后会立即停止,不会再走第三阶段。另外 respondsToSelector: 的判定早于消息转发,如果类内部实现了 forwardingTargetForSelector: 但 respondsToSelector: 返回 NO,那么调用时还是会转发?实际上 respondsToSelector 内部会检查本类及父类方法列表,不触发转发,这是常见误解。