文章目录
前言
Objective-C的消息传递与消息转发是其动态特性的核心,基于运行时(Runtime)系统实现。本文将从底层机制出发,详细解析消息传递的完整流程及消息转发的三个关键阶段,并结合源码(如 objc/runtime
)和汇编层面进行深入探讨。
一、消息传递:objc_msgSend 的“查字典+递归找家长”流程
当调用 OC 对象的方法时(如 [obj doSomething]
),编译器会将其转换为 C 函数调用:
objc_msgSend(obj, @selector(doSomething));
objc_msgSend
是 OC 消息传递的核心函数,其本质是在接收者的类及其父类的方法列表中查找目标方法(SEL)的实现(IMP),并执行该实现。我们下面以OC 中调用方法(如 [dog 叫]
)为例子,本质是让系统帮我们找到方法的实现代码(IMP)并执行。这个过程由 objc_msgSend
函数完成,它的执行逻辑像“查字典+递归找家长”,分三步:
1. 第一步:查“最近调用记录”(方法缓存)—— 最快即快速查找!
OC 运行时会为每个类维护一个方法缓存(methodCache_t
),用于加速方法查找。缓存的结构是一个哈希表,键为 SEL
(方法选择子),值为 IMP
(方法实现的指针)。
objc_msgSend
首先检查接收者类的缓存:
- 若缓存中存在目标
SEL
,直接跳转到对应的IMP
执行(零成本缓存命中)。 - 若缓存未命中,进入类方法列表查找
例如,每个类(如 Dog
类)都有一个 方法缓存(Method Cache),类似手机的“最近通话记录”:
- 作用:存“最近调用过的方法名(SEL)”和对应的“实现代码地址(IMP)”,下次调用直接查缓存,无需重复计算。
- 为什么快:哈希表结构,查找时间复杂度接近 O(1)(常数级)。
例子:
你上周让 dog
叫过 3 次,系统就把“叫”这个方法名(SEL)和对应的“汪汪汪”实现(IMP)记在 Dog
类的缓存里。这周再调用 [dog 叫]
,objc_msgSend
直接查缓存,秒级找到 IMP 并执行。
2. 第二步:翻“自己的字典”(类方法列表查找)—— 较慢!
若缓存未命中,objc_msgSend
会从接收者的当前类开始,逐级向上遍历继承链(直到 NSObject
或根类),在每个类的方法列表(method_list_t
)中查找目标 SEL
。
每个类的方法列表存储了该类自身定义的方法(不包括父类)。若当前类未找到,继续查找其父类的方法列表,直到根类(如 NSObject
)的父类为 nil
,此时查找失败。
例子:
如果缓存里没找到(比如第一次调用 [dog 叫]
),objc_msgSend
会去当前类的“字典”(方法列表)里找。每个类的方法列表存着自己定义的所有方法(类似字典的“正文”)。 Dog
类的字典里有 叫
、跑
等方法的定义(SEL 是“叫”,IMP 是“汪汪汪”的代码)。objc_msgSend
遍历这个字典,找到“叫”对应的 IMP,执行。
3. 第三步:递归“找家长”(父类方法列表)—— 最慢!
如果当前类的字典里也没有(比如 Dog
类没写 叫
方法),objc_msgSend
会去父类的字典里继续找(类似“问爸爸有没有这个词的解释”)。一直找到根类(如 NSObject
)的父类(nil
),若最终找到目标 SEL
的 IMP
,则将该 SEL
与 IMP
的映射写入当前类的方法缓存(后续调用直接命中缓存),并跳转到 IMP
执行方法逻辑;还没找到,就触发消息转发。
总结:消息传递的“三级跳”
调用 [dog 叫] → objc_msgSend 开始:
1.查 Dog 类的缓存 → 找到?直接执行(最快)。
2.没找到 → 查 Dog 类的方法列表 → 找到?执行(较快)。
3.没找到 → 递归查父类(Animal → NSObject)的方法列表 → 找到?执行(较慢)。
4.全没找到 → 触发消息转发(兜底逻辑)。
二、消息转发:快递送不到时的“三级补救方案”
若 objc_msgSend
遍历完缓存、当前类、父类继承链仍未找到目标 SEL
的 IMP
,OC 运行时会触发**消息转发(Message Forwarding)**机制,尝试通过一系列回调让开发者有机会“补救”未处理的消息。消息转发分为三个阶段,按顺序执行且不可逆(前一阶段成功则后续阶段不再触发)。
阶段 1:动态方法解析(自己加方法)——“我马上补一个!”
Objective-C(OC)的消息传递与消息转发是其动态特性的核心,基于运行时(Runtime)系统实现。本文将从底层机制出发,详细解析消息传递的完整流程及消息转发的三个关键阶段,并结合源码(如 objc/runtime
)和汇编层面进行深入探讨。
一、消息传递的本质:objc_msgSend
的执行流程
当调用 OC 对象的方法时(如 [obj doSomething]
),编译器会将其转换为 C 函数调用:
objc_msgSend(obj, @selector(doSomething));
objc_msgSend
是 OC 消息传递的核心函数,其本质是在接收者的类及其父类的方法列表中查找目标方法(SEL)的实现(IMP),并执行该实现。整个过程可分为以下步骤:
1. 快速查找:方法缓存(Method Cache)
OC 运行时会为每个类维护一个方法缓存(methodCache_t
),用于加速方法查找。缓存的结构是一个哈希表,键为 SEL
(方法选择子),值为 IMP
(方法实现的指针)。
objc_msgSend
首先检查接收者类的缓存:
- 若缓存中存在目标
SEL
,直接跳转到对应的IMP
执行(零成本缓存命中)。 - 若缓存未命中,进入类方法列表查找。
2. 类方法列表查找
若缓存未命中,objc_msgSend
会从接收者的当前类开始,逐级向上遍历继承链(直到 NSObject
或根类),在每个类的方法列表(method_list_t
)中查找目标 SEL
。
每个类的方法列表存储了该类自身定义的方法(不包括父类)。若当前类未找到,继续查找其父类的方法列表,直到根类(如 NSObject
)的父类为 nil
,此时查找失败。
3. 缓存更新与结果返回
若最终找到目标 SEL
的 IMP
,则将该 SEL
与 IMP
的映射写入当前类的方法缓存(后续调用直接命中缓存),并跳转到 IMP
执行方法逻辑。
二、消息转发:当消息无法被处理时
若 objc_msgSend
遍历完继承链仍未找到目标 SEL
的 IMP
,OC 运行时会触发**消息转发(Message Forwarding)**机制,尝试通过一系列回调让开发者有机会“补救”未处理的消息。消息转发分为三个阶段,按顺序执行且不可逆(前一阶段成功则后续阶段不再触发)。
阶段 1:动态方法解析(Dynamic Method Resolution)
运行时首先调用类的类方法 +resolveInstanceMethod:
(针对实例方法)或 +resolveClassMethod:
(针对类方法),允许开发者动态添加方法实现。系统先问当前类:“你能自己写一个这个方法吗?”(调用 +resolveInstanceMethod:
)。这时候我们可以用 class_addMethod
动态添加方法实现,相当于“临时补字典条目”。
例子:
我们发现 Dog
类忘记实现 叫
方法,于是在 +resolveInstanceMethod:
里补上:
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(叫)) {
// 动态添加方法:SEL 是“叫”,IMP 是“汪汪汪”的代码
class_addMethod(self, sel, (IMP)叫的实现, "v@:");
return YES; // 返回YES,表示消息已经被处理,即告诉系统:“我自己解决了!”,objc_msgSend会重新尝试发送消息(此时缓存已更新)。
}
return [super resolveInstanceMethod:sel];
}
// 方法实现(IMP)
void 叫的实现(id self, SEL _cmd) {
NSLog(@"汪汪汪!");
}
如果成功,系统会把新方法加入缓存,下次调用直接命中。
阶段 2:快速转发(转交给其他对象)——“我找朋友帮忙!”
如果动态解析失败(+resolveInstanceMethod:
返回 NO
),运行时会调用实例方法 -forwardingTargetForSelector:
,允许开发者指定一个备用接收者(Forwarding Target),将消息转发给该对象处理。动态解析失败比如你不想自己加方法,系统问:“你能找个朋友(其他对象)帮我处理吗?”(调用 -forwardingTargetForSelector:
)。你返回一个能处理该消息的对象,相当于“把快递转交给邻居”。
例子:
Dog
类发现自己不会“叫”,但它的朋友 Cat
类会,于是返回 Cat
的实例:
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(叫)) {
return [Cat new]; // 找 Cat 帮忙
}
return [super forwardingTargetForSelector:aSelector];
}
若返回非 nil
对象,消息会被发送给该对象(相当于“代理”模式);若返回 nil
,进入下一阶段。上述代码中系统会把 [dog 叫]
转发给 Cat
对象,如果 Cat
会“叫”,消息就被正确处理。
阶段 3:完整转发(自定义处理流程)——“我自己写个转单系统!”
若前两阶段均失败(一般是快速转发未提供备用接收者),运行时会触发完整的消息转发流程,核心是构造一个 NSInvocation
对象封装消息信息,并调用 -forwardInvocation:
方法让开发者自定义处理。就例如你找不到能帮忙的对象,系统启动“完整转发”:把消息(谁发的、方法名、参数)打包成 NSInvocation
对象,调用 -forwardInvocation:
让你自定义处理。你需要自己决定如何处理这个消息(比如转给其他对象、修改参数、记录日志)。
例子:
你重写 -forwardInvocation:
,把消息转给 Cat
,并记录日志:
- (void)forwardInvocation:(NSInvocation *)invocation {
// 1. 获取原消息的信息(方法名、参数)
SEL sel = invocation.selector;
id target = [Cat new]; // 临时目标
// 2. 修改消息目标为 Cat
[invocation setTarget:target]; // 改成转给 Cat
[invocation invoke]; // 重新发送消息
// 3. (可选)获取返回值并处理
id result;
[invocation getReturnValue:&result];
NSLog(@"转发成功,结果是:%@", result);
}
// 必须实现:获取目标方法的签名(NSMethodSignature),用于描述方法的参数、返回值类型等信息。若未实现此方法,会直接抛出 unrecognized selector异常。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(叫)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"]; // 无参数,返回 void
}
return [super methodSignatureForSelector:aSelector];
}
若 -methodSignatureForSelector:
未实现或返回 nil
,运行时会直接抛出 NSInvalidArgumentException
(unrecognized selector sent to instance
)。
关键注意点:
- 如果
-methodSignatureForSelector:
没实现或返回nil
,系统会直接抛出unrecognized selector sent to instance
崩溃(常见错误)。
三、底层原理:用汇编看 objc_msgSend
的“高效魔法”
objc_msgSend
是用 ARM64 汇编 写的,核心逻辑用几行伪代码概括:
objc_msgSend:
// 1. 检查接收者是否为 nil(OC 允许向 nil 发消息)
cbz x0, LReturnNil // 如果 receiver 是 nil,直接返回 0
// 2. 查缓存:从 receiver 的 isa 指针找到类,然后在缓存里找 SEL
ldr x1, [x0] // x1 = receiver->isa(类的地址)
CacheLookup // 汇编指令:在类的缓存里查 SEL 对应的 IMP
// 3. 缓存命中:直接跳转到 IMP 执行
br x2 // x2 是缓存的 IMP 地址,跳转执行
LReturnNil:
mov x0, #0 // 返回 0(对应 nil 消息的处理)
ret
为什么快:缓存查找是汇编级别的优化,几乎无额外开销;方法列表查找是递归遍历,但仅在缓存未命中时触发。
四、总结
OC 的消息传递与转发机制,本质是 “运行时动态性” 的体现:
- 高效性:通过缓存和方法列表的层级查找,平衡了“首次调用”和“重复调用”的性能。
- 灵活性:消息转发的三阶段设计,允许开发者在运行时动态修复未处理的方法(如 KVO、动态代理)。
一句话总结:objc_msgSend
像一个“智能快递员”,先查最近记录(缓存),再翻自己家抽屉(方法列表),最后递归问家长(父类);找不到时,系统给你三次“补救机会”(动态解析→快速转发→完整转发),确保消息“不轻易丢失”。**