【iOS】消息传递和消息转发

发布于:2025-07-19 ⋅ 阅读:(19) ⋅ 点赞:(0)

前言

  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),若最终找到目标 SELIMP,则将该 SELIMP的映射写入当前类的方法缓存(后续调用直接命中缓存),并跳转到 IMP执行方法逻辑;还没找到,就触发消息转发。

总结:消息传递的“三级跳”

调用 [dog 叫] → objc_msgSend 开始:

1.查 Dog 类的缓存 → 找到?直接执行(最快)。

2.没找到 → 查 Dog 类的方法列表 → 找到?执行(较快)。

3.没找到 → 递归查父类(Animal → NSObject)的方法列表 → 找到?执行(较慢)。

4.全没找到 → 触发消息转发(兜底逻辑)。

二、消息转发:快递送不到时的“三级补救方案”

objc_msgSend遍历完缓存、当前类、父类继承链仍未找到目标 SELIMP,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. 缓存更新与结果返回

若最终找到目标 SELIMP,则将该 SELIMP的映射写入当前类的方法缓存(后续调用直接命中缓存),并跳转到 IMP执行方法逻辑。

二、消息转发:当消息无法被处理时

objc_msgSend遍历完继承链仍未找到目标 SELIMP,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,运行时会直接抛出 NSInvalidArgumentExceptionunrecognized 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 像一个“智能快递员”,先查最近记录(缓存),再翻自己家抽屉(方法列表),最后递归问家长(父类);找不到时,系统给你三次“补救机会”(动态解析→快速转发→完整转发),确保消息“不轻易丢失”。**


网站公告

今日签到

点亮在社区的每一天
去签到