一、核心前提: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 快速查找流程总结(汇编层面)
- 校验消息接收者 self:判断是否为 nil 或 tagged pointer,nil 则直接返回 0,避免崩溃;
- 获取类对象:通过 self 的 isa 指针,提取当前对象的类(class),存储到 p16;
- 哈希计算:通过 SEL 与 cache_t 的 mask 做与运算,得到哈希索引,定位到 buckets 中的目标桶;
- 匹配 SEL:加载桶中的 SEL 与目标 SEL 对比,匹配则执行 IMP(CacheHit),不匹配则线性探测下一个桶;
- 查找终止:若遍历所有桶仍未找到,触发 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,步骤如下:
- 当前类方法查找:调用
getMethodNoSuper_nolock方法,在当前类的方法列表(methodList)中查找 SEL,采用二分查找算法(方法列表已排序),效率较高; - 父类切换:若当前类未找到,将 curClass 切换为当前类的父类,若父类为 nil,说明整个父类链都未找到,触发消息转发;
- 父类缓存检查:切换父类后,先检查父类的缓存(快速查找),若找到则缓存到当前类,提升后续查找效率;
- 循环终止条件:找到方法 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),是性能优化的关键。
六、整体流程梳理(从方法调用到转发)
- OC 方法调用
[obj method]编译为objc_msgSend(obj, @selector(method)); - 汇编层面校验 self,获取类对象,通过 CacheLookup 宏在类的缓存中快速查找 IMP;
- 缓存命中:执行 IMP,流程结束;
- 缓存未命中:跳至
objc_msgSend_uncached,调用lookUpImpOrForward进入慢速查找; - 慢速查找:遍历当前类方法列表、父类链(缓存 + 方法列表),找到则缓存 IMP 并执行;
- 未找到:尝试动态方法决议,决议成功则重新查找,失败则触发消息转发;
- 消息转发:若未实现转发逻辑,最终调用
_objc_msgForward_impcache,抛出 unrecognized selector 异常。