消息发送与转发流程

一、核心前提:OC 消息发送的本质

在 Objective-C 中,所有方法调用的底层都会转换为 objc_msgSend 函数的调用,这是 OC 动态消息机制的核心载体。不同于传统函数调用的直接绑定,OC 方法调用属于"动态绑定"------在运行时才确定方法的具体实现(IMP),而这一过程的核心执行者就是 objc_msgSend

例如,OC 中的方法调用 [myObject test1][myObject test2:100],编译后会被转换为 C 语言层面的 objc_msgSend 调用,简化后如下:

less 复制代码
// 无参数方法
objc_msgSend(myObject, sel_registerName("test1"));
// 带参数方法
objc_msgSend(myObject, sel_registerName("test2:"), 100);

objc_msgSend 的官方定义如下,其核心作用是接收消息接收者(self)、方法选择器(SEL),以及可变参数,最终找到对应的方法实现(IMP)并执行:

python 复制代码
OBJC_EXPORT id _Nullable
objc_msgSend(id _Nullable self, SEL _Nonnull op, ...);

值得注意的是,objc_msgSend 采用汇编语言实现,而非 C/C++,核心原因有两点:一是汇编对可变参数的处理更灵活,能适配不同返回值类型(如 id、int、float 等)的方法;二是汇编执行效率极高,可满足 objc_msgSend 高频调用的性能需求。其核心逻辑可用伪代码简化为:

python 复制代码
id objc_msgSend(id self, SEL _cmd, ...) {
    Class class = object_getClass(self);
    IMP imp = class_getMethodImplementation(class, _cmd);
    return imp ? imp(self, _cmd, ...) : 0;
}

二、objc_msgSend 完整汇编实现(ARM64 架构)

objc_msgSend 的汇编源码主要位于 objc-msg-arm64.s 文件中,核心流程分为「参数校验」「快速查找」两大阶段,以下是完整汇编代码解析(关键步骤加注释),贴合苹果开源源码逻辑:

less 复制代码
// 入口函数,标记函数开始
ENTRY _objc_msgSend
//  unwind 指令:用于调试时的栈回溯,标记无帧
UNWIND _objc_msgSend, NoFrame

// 第一步:校验消息接收者(self)是否为 nil 或 tagged pointer
cmp p0, #0                  // p0 存储第一个参数 self,比较 self 与 0
#if SUPPORT_TAGGED_POINTERS  // 判断是否支持 tagged pointer(小对象优化)
b.le LNilOrTagged           // 若 self <= 0,跳至 LNilOrTagged 处理(nil 或 tagged pointer)
#else
b.eq LReturnZero            // 不支持 tagged pointer 时,self == 0 直接返回 0
#endif

// 第二步:获取消息接收者的 isa 指针,进而获取其类对象
ldr p13, (x0)               // x0 = self,将 self 的 isa 指针加载到 p13(isa 存储在 self 首地址)
GetClassFromIsa_p16 p13     // 宏定义,从 isa 中提取类对象,存储到 p16(p16 = class)
LGetIsaDone:                // 获取类对象完成,进入缓存查找

// 第三步:缓存快速查找(核心宏,传入 NORMAL 表示常规查找)
CacheLookup NORMAL          // 调用 CacheLookup 宏,查找缓存中的 IMP,找到则执行,未找到则跳至慢速查找

// -------------- 分支1:处理 nil 或 tagged pointer --------------
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
b.eq LReturnZero            // 若 self 是 nil,跳至 LReturnZero 返回 0
// 处理 tagged pointer(小对象优化,如 NSNumber、NSString 等),简化逻辑如下
// 提取 tagged pointer 中的类信息,重新赋值给 p16
ldr p16, [x10, #8]
cmp x10, x16
b.ne LGetIsaDone
// 扩展 tagged pointer 处理,最终跳回缓存查找
b LGetIsaDone
#endif

// -------------- 分支2:self 为 nil 时返回 0 --------------
LReturnZero:
// 初始化返回值为 0(适配不同返回类型:x0 对应 id/指针,d0-d3 对应浮点型)
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
ret                         // 函数返回,nil 发送消息不崩溃的核心原因

// 标记函数结束
END_ENTRY _objc_msgSend

2.1 关键汇编宏解析:CacheLookup(快速查找核心)

CacheLookup 是汇编实现快速查找的核心宏,其作用是在类对象的缓存(cache_t)中,根据 SEL 查找对应的 IMP。缓存的核心结构是 cache_t,内部包含 buckets(哈希桶,存储 IMP+SEL 键值对)、mask(哈希掩码)、occupied(已占用数量),查找采用哈希算法,效率接近 O(1)。以下是 CacheLookup 宏的核心逻辑解析(简化版):

csharp 复制代码
.macro CacheLookup  // 宏定义,接收查找类型(如 NORMAL)
// 准备参数:p1 = SEL(_cmd),p16 = 类对象(class)
ldp p10, p11, (x16, #CACHE)  // 从类对象中加载 cache_t:p10 = buckets(哈希桶数组),p11 = occupied | mask
#if !__LP64__
and w11, w11, 0xffff        // 32位系统中,mask 仅占低16位,需过滤高位
#endif
// 哈希计算:SEL & mask = 哈希索引(确定 SEL 在 buckets 中的位置)
and w12, w1, w11            // x12 = _cmd & mask(哈希索引)
// 计算 bucket 地址:buckets + 索引 * 桶大小(每个桶存储 IMP+SEL,共 16 字节)
add p12, p10, p12, LSL #(1+PTRSHIFT)  // PTRSHIFT=3(64位),LSL 4 等价于 *16

// 加载当前桶的 IMP 和 SEL:p17 = IMP,p9 = SEL
ldp p17, p9, (x12)
1: cmp p9, p1                // 比较当前桶的 SEL 与目标 SEL(p1 = _cmd)
b.ne 2f                     // 不相等,跳至 2f 继续查找下一个桶
CacheHit $0                 // 找到匹配 SEL,调用 CacheHit 宏,执行 IMP 并返回

2: // 未找到,处理哈希冲突(线性探测法,继续查找下一个桶)
CheckMiss $0                // 检查是否查找完毕,未找到则跳至慢速查找(objc_msgSend_uncached)
// 线性探测:桶地址 + 16(下一个桶),循环查找
add p12, p12, #(1 << (1+PTRSHIFT))  // 等价于 p12 += 16
ldp p17, p9, (x12)          // 加载下一个桶的 IMP 和 SEL
b 1b                        // 跳回 1:,继续比较 SEL
.endmacro

2.2 快速查找流程总结(汇编层面)

  1. 校验消息接收者 self:判断是否为 nil 或 tagged pointer,nil 则直接返回 0,避免崩溃;
  2. 获取类对象:通过 self 的 isa 指针,提取当前对象的类(class),存储到 p16;
  3. 哈希计算:通过 SEL 与 cache_t 的 mask 做与运算,得到哈希索引,定位到 buckets 中的目标桶;
  4. 匹配 SEL:加载桶中的 SEL 与目标 SEL 对比,匹配则执行 IMP(CacheHit),不匹配则线性探测下一个桶;
  5. 查找终止:若遍历所有桶仍未找到,触发 CheckMiss 宏,跳至慢速查找流程(objc_msgSend_uncached)。

三、慢速查找流程(C/C++ 层面)

当快速查找(汇编缓存查找)未找到对应的 IMP 时,会进入慢速查找流程。慢速查找由 C/C++ 实现,核心函数是 lookUpImpOrForward(位于 objc-runtime-new.mm 文件),其核心逻辑是「遍历类的方法列表 + 父类链查找」,若仍未找到则触发消息转发。

慢速查找的触发路径:汇编层面 CacheLookupCheckMissobjc_msgSend_uncached → 调用 lookUpImpOrForward(C/C++ 函数)。

3.1 lookUpImpOrForward 完整源码解析(关键步骤注释)

scss 复制代码
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior) {
    // 定义消息转发的 IMP(未找到方法时,最终会跳转到这个实现)
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;

    // 断言:确保当前未持有运行时锁(初始状态无锁)
    runtimeLock.assertUnlocked();

    // 第一步:再次检查缓存(防止多线程场景下,查找期间缓存被更新)
    if (fastpath(behavior & LOOKUP_CACHE)) {
        imp = cache_getImp(cls, sel);
        if (imp) goto done_nolock;  // 找到缓存,直接返回
    }

    // 第二步:加锁,保证线程安全(避免多线程操作类结构、方法列表)
    runtimeLock.lock();

    // 校验类的合法性:确保当前类是已加载、已认可的类
    checkIsKnownClass(cls);

    // 第三步:确保类已实现(realize),若未实现则初始化类结构、确定父类链
    if (slowpath(!cls->isRealized())) {
        cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
    }

    // 第四步:确保类已初始化(initialize),若未初始化则执行 +initialize 方法
    if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
        cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
    }

    runtimeLock.assertLocked();
    curClass = cls;

    // 第五步:循环遍历类及其父类链,查找方法(核心逻辑)
    // unreasonableClassCount:类迭代上限,防止父类链循环导致死循环
    for (unsigned attempts = unreasonableClassCount();;) {
        // 5.1 查找当前类的方法列表(采用二分查找,效率高)
        Method meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
            imp = meth->imp;  // 找到方法,获取其 IMP
            goto done;        // 跳至 done,缓存 IMP 并返回
        }

        // 5.2 若当前类无该方法,获取父类,继续查找
        if (slowpath((curClass = curClass->superclass) == nil)) {
            // 父类为 nil,仍未找到,触发消息转发
            imp = forward_imp;
            break;
        }

        // 5.3 防止父类链循环(如类的 superclass 指向自身)
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }

        // 5.4 检查父类的缓存(再次尝试快速查找,优化性能)
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) {
            // 父类中找到的是转发 IMP,停止查找,优先执行当前类的方法解析
            break;
        }

        // 5.5 父类缓存中找到 IMP,跳至 done 缓存并返回
        if (fastpath(imp)) {
            goto done;
        }
    }

    // 第六步:未找到方法,尝试动态方法决议(仅执行一次)
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;  // 标记已执行过决议,避免重复执行
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

done:
    // 第七步:将找到的 IMP 缓存到当前类的 cache 中,供下次快速查找
    log_and_fill_cache(cls, imp, sel, inst, curClass);
    // 解锁
    runtimeLock.unlock();

done_nolock:
    // 处理 nil 场景:若未找到方法且允许返回 nil,返回 nil(否则返回转发 IMP)
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;
}

3.2 慢速查找核心流程拆解

3.2.1 前置校验与准备(线程安全 + 类初始化)

慢速查找前会做3件关键准备工作,确保查找的合法性和安全性:

  • 二次缓存检查:防止多线程场景下,快速查找后缓存被更新,再次检查缓存可提升性能;
  • 加锁:通过 runtimeLock 加锁,避免多线程同时操作类的方法列表、父类链,保证线程安全;
  • 类初始化校验:确保类已实现(realize)和初始化(initialize),未实现则初始化类结构、确定父类链,未初始化则执行 +initialize 方法。

3.2.2 核心循环:类与父类链遍历查找

这是慢速查找的核心,通过 for 循环遍历当前类及其所有父类,直到找到方法或父类为 nil,步骤如下:

  1. 当前类方法查找:调用 getMethodNoSuper_nolock 方法,在当前类的方法列表(methodList)中查找 SEL,采用二分查找算法(方法列表已排序),效率较高;
  2. 父类切换:若当前类未找到,将 curClass 切换为当前类的父类,若父类为 nil,说明整个父类链都未找到,触发消息转发;
  3. 父类缓存检查:切换父类后,先检查父类的缓存(快速查找),若找到则缓存到当前类,提升后续查找效率;
  4. 循环终止条件:找到方法 IMP、父类为 nil(触发转发)、父类链循环(报错)。

3.2.3 动态方法决议(可选分支)

若遍历类和父类链仍未找到方法,不会直接触发转发,而是先尝试「动态方法决议」------允许开发者在运行时动态添加方法实现,避免消息转发。

动态方法决议的核心函数是resolveMethod_locked,会分别调用类方法 +resolveInstanceMethod:(实例方法)或 +resolveClassMethod:(类方法),开发者可在这两个方法中动态添加 SEL 对应的 IMP。若决议成功(返回 YES 并添加 IMP),则重新进入查找流程;若决议失败,则正式触发消息转发。

3.2.4 缓存写入(优化后续查找)

无论在当前类方法列表找到 IMP,还是在父类缓存/方法列表找到 IMP,最终都会通过 log_and_fill_cache 方法,将 IMP 缓存到当前类的 cache_t 中。这样下次再调用该方法时,可通过汇编快速查找直接获取 IMP,提升性能。

四、汇编快速查找与慢速查找关联总结

对比维度 汇编快速查找 C/C++ 慢速查找
实现语言 汇编(ARM64/x86) C/C++
查找范围 当前类的 cache_t(哈希桶) 当前类方法列表 + 整个父类链(缓存 + 方法列表)
查找效率 极高(O(1),哈希查找) 中等(O(n),遍历父类链 + 二分查找方法列表)
核心作用 处理高频方法调用,快速命中缓存 缓存未命中时,完整查找方法,支持动态决议
触发关系 优先执行,未命中则触发慢速查找 快速查找失败后执行,失败则触发消息转发

五、关键补充:常见问题与注意点

  • nil 发送消息不崩溃的原因:汇编层面会校验 self 是否为 nil,若为 nil 则直接返回 0,不执行后续查找和调用逻辑,因此不会崩溃;
  • tagged pointer 处理:对于 tagged pointer 优化的小对象(如 NSNumber),会提取其内部的类信息,重新进入缓存查找流程,避免额外的对象创建;
  • 线程安全保障:慢速查找中通过 runtimeLock 加锁,防止多线程操作类结构和方法列表,避免数据错乱;
  • 动态方法决议的优先级:动态方法决议在慢速查找之后、消息转发之前,仅执行一次,开发者可通过它动态修复未实现的方法;
  • 缓存写入的意义:将慢速查找找到的 IMP 缓存到当前类,可将后续调用的查找效率从 O(n) 提升到 O(1),是性能优化的关键。

六、整体流程梳理(从方法调用到转发)

  1. OC 方法调用 [obj method] 编译为 objc_msgSend(obj, @selector(method))
  2. 汇编层面校验 self,获取类对象,通过 CacheLookup 宏在类的缓存中快速查找 IMP;
  3. 缓存命中:执行 IMP,流程结束;
  4. 缓存未命中:跳至 objc_msgSend_uncached,调用 lookUpImpOrForward 进入慢速查找;
  5. 慢速查找:遍历当前类方法列表、父类链(缓存 + 方法列表),找到则缓存 IMP 并执行;
  6. 未找到:尝试动态方法决议,决议成功则重新查找,失败则触发消息转发;
  7. 消息转发:若未实现转发逻辑,最终调用 _objc_msgForward_impcache,抛出 unrecognized selector 异常。
相关推荐
jerrywus1 天前
别再陪 AI 调 iOS 了:用 cmux + baguette,让 Claude 在你的模拟器里"自己动手"
前端·ios·claude
MonkeyKing71551 天前
iOS 开发 Block 底层结构、循环引用及解决方案
ios·面试
文件夹__iOS1 天前
Swift 5.9 被严重低估的特性:参数包,一次性干掉重复泛型重载
ios·swiftui·swift
薛定猫AI1 天前
【技术干货】用 AI + Expo 打通 iOS / Android / Web 跨端应用开发:从架构到代码生成实战
android·人工智能·ios
MonkeyKing1 天前
iOS关联对象底层实现与内存管理细节
ios
90后的晨仔2 天前
SwiftUI 高级特性第2章:组合与容器
ios
pop_xiaoli2 天前
【iOS】SDWebImage源码
macos·ios·objective-c·cocoa
移动端小伙伴3 天前
我受够了 Xcode 的 SPM 网络问题,写了个脚本一劳永逸
ios
人月神话-Lee3 天前
两个改动,让这个iOS OCR SDK识别成功率翻了一倍
ios·ocr·ai编程·身份证识别·银行卡识别