Objective-C 的 消息发送机制 (objc_msgSend 流程)
Objective-C 是一门动态语言,其核心机制就是消息发送 (Messaging)。在 C 语言中,函数调用在编译期就决定了跳转地址;而在 Objective-C 中,[obj foo] 并不会立即执行 foo 方法的代码,而是向 obj 发送了一条名为 foo 的消息。
这个过程的核心函数就是 objc_msgSend。整个流程大致可以分为三大阶段:
- 消息发送阶段 (Messaging / Lookup):查找方法的实现 (IMP)。
- 动态方法决议阶段 (Dynamic Method Resolution):如果找不到,给个机会动态添加方法。
- 消息转发阶段 (Message Forwarding):如果还没添加,将消息转发给其他对象处理。
0. 编译阶段
编译器会将你的代码转换成 C 语言函数调用:
// 源代码
[receiver message:arg];
// 编译器转换后
objc_msgSend(receiver, @selector(message:), arg);
objc_msgSend 的原型为:id objc_msgSend(id self, SEL op, ...)
1. 消息发送阶段 (Messaging)
这是最频繁发生的阶段,为了性能,苹果在 Runtime 中使用了大量的汇编代码来优化这一步。
1.1 判断 receiver 是否为 nil
- 如果
receiver是nil,objc_msgSend会直接返回(根据返回值类型返回 0、nil 或 void),不会进行后续查找。这就是为什么给 nil 发送消息不会崩溃的原因。
1.2 查找 Cache (快速路径 - Fast Path)
- Runtime 会根据
receiver的isa指针找到它所属的 Class。 - 每个 Class 都有一个 方法缓存 (
cache_t),用于存储最近调用过的方法。 - 系统使用
SEL(方法编号) 通过哈希算法计算出在缓存 Hash Table 中的索引。 - 命中:如果能在缓存中找到对应的
Bucket(包含 key 和 IMP),则直接跳转到IMP执行。这是最快的情况。 - 未命中:如果缓存中没有,则进入慢速查找路径。
1.3 查找方法列表 (慢速路径 - Slow Path)
- 这一步通常进入 C++ 函数
lookUpImpOrForward。 - 当前类查找:在当前 Class 的
method_list(方法列表) 中查找。- 如果是已排序列表,使用二分查找。
- 如果是未排序列表,使用线性遍历。
- 如果找到:填充到 Cache 中,并跳转执行 IMP。
- 父类查找:如果当前类没找到,则通过
superclass指针找到父类。- 先查父类的 Cache。
- 再查父类的
method_list。 - 如果找到:填充到 Receiver 的 Class 的 Cache 中(注意不是父类的 Cache),并执行。
- 循环向上:一直重复上述步骤,直到根类 (
NSObject)。 - 仍未找到:如果
NSObject中也找不到,进入动态方法决议阶段。
2. 动态方法决议阶段 (Dynamic Method Resolution)
如果方法查找失败,Runtime 会给类一个机会,在运行时动态添加方法实现。
- 实例方法:调用
+ (BOOL)resolveInstanceMethod:(SEL)sel - 类方法:调用
+ (BOOL)resolveClassMethod:(SEL)sel
操作流程:
- 开发者重写上述方法。
- 利用
class_addMethod动态添加方法的实现 (IMP)。 - 返回
YES(实际上 Runtime 只要看你有没有添加方法,返回值在现代 Runtime 中不是决定性因素,但规范通常返回 YES)。 - 系统会重新触发一次消息发送流程 (回到第 1 阶段)。如果这次找到了(因为你刚才添加了),就正常执行;如果还没找到,进入下一阶段。
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(foo)) {
class_addMethod(self, sel, (IMP)fooMethod, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
3. 消息转发阶段 (Message Forwarding)
如果动态决议没有解决问题(或者开发者没处理),系统会进入消息转发机制。这分为两步:快速转发和标准转发。
3.1 快速转发 (Fast Forwarding)
- 调用方法:
- (id)forwardingTargetForSelector:(SEL)aSelector - 目的:找一个“替身”对象来接收这个消息。
- 逻辑:如果返回一个非
nil且非self的对象,Runtime 会将消息重新发送给这个返回的对象。 - 特点:效率高,因为不需要创建
NSInvocation对象。
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(foo)) {
return otherObject; // 让 otherObject 去处理 foo
}
return [super forwardingTargetForSelector:aSelector];
}
3.2 标准转发 (Normal Forwarding)
如果快速转发返回 nil,则进入最后且开销最大的完整转发过程。
方法签名:调用
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector。- 你需要返回一个方法签名对象,描述方法的返回值和参数类型。
- 如果返回
nil,则直接崩溃,抛出unrecognized selector sent to instance。
消息调用:如果签名不为空,Runtime 会创建一个
NSInvocation对象(封装了 target, selector, arguments),然后调用- (void)forwardInvocation:(NSInvocation *)anInvocation。- 在这里,你可以随心所欲地处理消息:修改目标、修改参数、甚至什么都不做(吞掉消息)。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(foo)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
// 可以指定其他对象处理
[anInvocation invokeWithTarget:someOtherObject];
}
4. 最终结果:崩溃
如果 forwardInvocation: 也没有处理(或者 methodSignatureForSelector: 返回 nil),最终会调用 doesNotRecognizeSelector:,抛出经典的异常:
unrecognized selector sent to instance 0x...
总结图解
[obj foo]
|
V
1. 判断 obj 是否为 nil? -> YES -> return (0/nil)
| NO
V
2. 查找 obj->isa (Class) 的 Cache? -> YES -> 执行 IMP
| NO
V
3. 查找 Class 的 method_list? -> YES -> 填充 Cache -> 执行 IMP
| NO
V
4. 递归查找 SuperClass (Cache & List)? -> YES -> 填充 Cache -> 执行 IMP
| NO (直到 NSObject 也没找到)
V
5. 动态方法决议 (+resolveInstanceMethod:)? -> 添加了方法 -> 重新开始查找
| NO
V
6. 快速转发 (-forwardingTargetForSelector:)? -> 返回新对象 -> objc_msgSend(新对象, foo)
| NO (返回 nil)
V
7. 方法签名 (-methodSignatureForSelector:)? -> 返回 nil -> Crash
| YES (返回签名)
V
8. 标准转发 (-forwardInvocation:)? -> 开发者手动处理
| 不处理
V
9. Crash (doesNotRecognizeSelector:)