作为 iOS 开发者,我们每天都在写
[receiver message]。但你是否想过,这行简单的代码背后,Runtime 究竟做了哪些惊心动魄的操作?
Objective-C 是一门动态语言,这意味着方法的调用并不是在编译期决定的,而是在运行时决定的。这个机制的核心就是 objc_msgSend。今天,我们就来扒开 Runtime 的源码,彻底搞懂消息发送的全过程。
0. 编译期的转换
首先,我们看一段简单的调用代码:
Person *person = [[Person alloc] init];
[person run];
在编译阶段,Clang 编译器会将 [person run] 转换为 C 语言的函数调用。
-
普通调用:
objc_msgSend(person, @selector(run)); -
objc_msgSend的原型:void objc_msgSend(id self, SEL op, ...);
这里的 self (接收者) 和 op (_cmd,方法选择子) 是两个隐藏参数,这就是为什么我们在方法内部可以使用 self 和 _cmd 的原因。
1. 消息发送流程总览
当 objc_msgSend 被调用时,Runtime 会按照以下顺序执行,直到找到方法的实现(IMP)或者程序崩溃:
- 判断接收者是否为 nil
- 快速查找(Cache Lookup):在类的缓存中查找。
- 慢速查找(Method List Lookup):遍历类及其父类的方法列表。
- 动态方法解析(Dynamic Method Resolution):给类一个动态添加方法的机会。
-
消息转发(Message Forwarding):
- 快速转发(Forwarding Target)
- 完整转发(Normal Forwarding)
-
崩溃:
doesNotRecognizeSelector:
下面我们详细拆解每一个步骤。
2. 阶段一:消息查找(Message Lookup)
2.1 判断 nil (Nil Check)
objc_msgSend 是用汇编编写的(为了速度)。它执行的第一步就是检查 self 是否为 nil。
- 如果是
nil,函数直接返回(这就是为什么给 nil 发消息不会崩的原因)。 - 如果不是
nil,继续。
2.2 快速查找 (Cache Lookup)
Runtime 拿到 self 的 isa 指针,找到对应的类对象 (Class)。类对象中有一个 cache_t 结构体,用于缓存调用过的方法。
-
原理:通过
SEL & mask计算哈希下标,去哈希表(buckets)中查找对应的bucket_t。 -
命中:如果找到了
SEL,直接获取对应的IMP并调用。这是最快的情况。 - 未命中:如果缓存中没有,进入慢速查找流程。
2.3 慢速查找 (Slow Path)
进入 lookUpImpOrForward 函数。
-
当前类查找:遍历当前类(Class)的
method_lists。这是一个已排序的二维数组。如果找到了SEL,说明查找成功:-
填充缓存:将该方法缓存到
cache_t中,方便下次快速调用。 - 调用 IMP。
-
填充缓存:将该方法缓存到
-
父类查找:如果当前类没找到,沿着
superclass指针找到父类。- 先去父类的
cache中找。 - 如果没找到,遍历父类的
method_lists。
- 先去父类的
- 循环向上:重复上述过程,直到根类(NSObject)。
- 仍未找到:如果到了 NSObject 还没找到,说明该方法确实没有实现。此时进入动态决议阶段。
3. 阶段二:动态方法解析 (Dynamic Method Resolution)
在抛出异常之前,Runtime 给了我们第一次“自救”的机会。
系统会调用以下方法(取决于调用的是实例方法还是类方法):
+ (BOOL)resolveInstanceMethod:(SEL)sel+ (BOOL)resolveClassMethod:(SEL)sel
如何自救?
你可以在这个方法里利用 class_addMethod 动态地添加一个方法实现。
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(run)) {
// 动态添加 run 方法的实现
class_addMethod(self, sel, (IMP)dynamicRunMethod, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
如果该方法返回 YES(或者只要添加了方法),Runtime 会重新走一遍消息查找流程(此时就能在 Cache 或方法列表中找到了)。
4. 阶段三:消息转发 (Message Forwarding)
如果动态解析阶段什么都没做(返回 NO),Runtime 会进入消息转发流程。这是 iOS 开发者利用 Runtime 进行黑魔法(如多重继承效果、防崩溃组件)的核心区域。
4.1 快速转发 (Fast Forwarding)
Runtime 会调用:
- (id)forwardingTargetForSelector:(SEL)aSelector
含义:既然你自己处理不了,有没有其他对象能帮忙处理?
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(run)) {
// 让 Car 对象去处理 run 消息
return [Car new];
}
return [super forwardingTargetForSelector:aSelector];
}
如果返回了一个非 nil 且非 self 的对象,Runtime 会将消息直接发送给该对象。这步代价很小。
4.2 完整转发 (Normal Forwarding)
如果快速转发返回 nil,进入最后也是代价最大的一步。Runtime 需要先获取方法的签名,生成 NSInvocation 对象。
-
方法签名:调用
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector。- 如果返回
nil,直接崩溃。 - 如果返回签名,系统生成
NSInvocation对象(包含 target, selector, arguments)。
- 如果返回
-
转发调用:调用
- (void)forwardInvocation:(NSInvocation *)anInvocation。
在这里,你可以随心所欲地处理这个消息:
- 修改目标对象:
[anInvocation invokeWithTarget:otherObject]; - 修改参数。
- 甚至什么都不做(吞掉消息,避免崩溃)。
5. 终局:崩溃
如果 methodSignatureForSelector: 返回 nil,或者 forwardInvocation: 中没有进行处理且调用了 super,Runtime 最终会调用:
- (void)doesNotRecognizeSelector:(SEL)aSelector
程序抛出 unrecognized selector sent to instance 异常并崩溃。
总结图谱
为了方便记忆,我们可以将整个 objc_msgSend 过程总结为以下三个阶段:
| 阶段 | 关键函数/方法 | 核心作用 | 代价 |
|---|---|---|---|
| 1. 消息查找 |
objc_msgSend Cache Lookup Method List Lookup |
在本类及父类中寻找 IMP | 小 (汇编优化) |
| 2. 动态解析 | resolveInstanceMethod: |
运行时动态添加方法实现 | 中 |
| 3. 消息转发 |
forwardingTargetForSelector: forwardInvocation:
|
将消息转嫁给其他对象 | 大 (需创建对象) |
流程图 (Mermaid)
graph TD
A[objc_msgSend] --> B{self == nil?}
B -- Yes --> C[Return nil]
B -- No --> D{Cache Lookup}
D -- Hit --> E[Call IMP]
D -- Miss --> F{Method List Lookup<br>Current & Super Classes}
F -- Found --> G[Fill Cache & Call IMP]
F -- Not Found --> H{resolveInstanceMethod:}
H -- Method Added --> D
H -- No Implementation --> I{forwardingTargetForSelector:}
I -- Returns Object --> J[objc_msgSend(newObj)]
I -- Returns nil --> K{methodSignatureForSelector:}
K -- Returns Signature --> L[forwardInvocation:]
K -- Returns nil --> M[doesNotRecognizeSelector:]
M --> N[Crash]
结语
理解 objc_msgSend 不仅是面试中的高频考点,更是深入掌握 iOS 开发、编写高效代码以及开发 “防崩溃组件”(如通过 Hook 转发流程来捕获异常)的基础。希望这篇文章能帮你彻底打通 Runtime 的任督二脉!