消息发送与转发流程

一、核心前提: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 文件),其核心逻辑是「遍历类的方法列表 + 父类链查找」,若仍未找到则触发消息转发。

慢速查找的触发路径:汇编层面 CacheLookup → CheckMiss → objc_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 异常。
相关推荐
黑化旺仔14 小时前
【OC】Runtime:类元数据、加载初始化与消息派发
开发语言·macos·ios·objective-c
songgeb15 小时前
iOS App支持设置RTL语言的实践
ios·swift·ui kit
软泡芙15 小时前
【iOS】swift启动屏显示黑屏
ios
用户693717500138417 小时前
当你向领导申请 AI 编程工具预算时,如果领导追问“能提效多少,有量化数据吗”,你该怎么回答?
android·后端·ios
Layer17 小时前
一些脱离 Mac 依赖的 Agent 操作 iPhone 实现
ios·ai编程·测试
用户693717500138418 小时前
当我说 Claude 搭配 GLM-5.3-Flash 太好用了,同事却提醒我:你可能正在失去对代码的掌控
android·后端·ios
茶底世界之下19 小时前
一键裁方会丢掉什么?用 Awaking 给园林照片做两种画幅
ios
Marco_Marco13520 小时前
定位SDK接入实战:Android/iOS/鸿蒙三端怎么选、怎么接
android·ios·harmonyos
老李IT笔记1 天前
关掉设备上的 AI 扩展:两个键各管一层,配错了等于没配
人工智能·ios·智能手机
2501_915921431 天前
iOS应用耗电测试:使用Energy Log、Keymob和Instruments进行性能监控与能源消耗分析
android·ios·小程序·https·uni-app·iphone·webview