深入底层:彻底搞懂 Objective-C 消息发送(objc_msgSend)的全过程


作为 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)或者程序崩溃:

  1. 判断接收者是否为 nil
  2. 快速查找(Cache Lookup):在类的缓存中查找。
  3. 慢速查找(Method List Lookup):遍历类及其父类的方法列表。
  4. 动态方法解析(Dynamic Method Resolution):给类一个动态添加方法的机会。
  5. 消息转发(Message Forwarding)
    • 快速转发(Forwarding Target)
    • 完整转发(Normal Forwarding)
  6. 崩溃doesNotRecognizeSelector:

下面我们详细拆解每一个步骤。


2. 阶段一:消息查找(Message Lookup)

2.1 判断 nil (Nil Check)

objc_msgSend 是用汇编编写的(为了速度)。它执行的第一步就是检查 self 是否为 nil

  • 如果是 nil,函数直接返回(这就是为什么给 nil 发消息不会崩的原因)。
  • 如果不是 nil,继续。

2.2 快速查找 (Cache Lookup)

Runtime 拿到 selfisa 指针,找到对应的类对象 (Class)。类对象中有一个 cache_t 结构体,用于缓存调用过的方法。

  • 原理:通过 SEL & mask 计算哈希下标,去哈希表(buckets)中查找对应的 bucket_t
  • 命中:如果找到了 SEL,直接获取对应的 IMP 并调用。这是最快的情况。
  • 未命中:如果缓存中没有,进入慢速查找流程。

2.3 慢速查找 (Slow Path)

进入 lookUpImpOrForward 函数。

  1. 当前类查找:遍历当前类(Class)的 method_lists。这是一个已排序的二维数组。如果找到了 SEL,说明查找成功:
    • 填充缓存:将该方法缓存到 cache_t 中,方便下次快速调用。
    • 调用 IMP
  2. 父类查找:如果当前类没找到,沿着 superclass 指针找到父类。
    • 先去父类的 cache 中找。
    • 如果没找到,遍历父类的 method_lists
  3. 循环向上:重复上述过程,直到根类(NSObject)。
  4. 仍未找到:如果到了 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 对象。

  1. 方法签名:调用 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

    • 如果返回 nil,直接崩溃。
    • 如果返回签名,系统生成 NSInvocation 对象(包含 target, selector, arguments)。
  2. 转发调用:调用 - (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 的任督二脉!


转载请说明出处内容投诉
CSS教程网 » 深入底层:彻底搞懂 Objective-C 消息发送(objc_msgSend)的全过程

发表评论

欢迎 访客 发表评论

一个令你着迷的主题!

查看演示 官网购买