Objective-C 动态方法解析全流程梳理

基于 objc4 源码 · resolveMethod_locked / resolveInstanceMethod 详解

一、定位:消息发送的"三阶段"模型

Objective-C 的 objc_msgSend 在 cache 未命中后会进入慢路径,整个慢路径分三个阶段,每一阶段都是上一阶段失败后的"最后机会"。动态方法解析处于第二阶段。

① 常规查找(Method Lookup)

cls 沿 superclass 链向上,逐层调用 getMethodNoSuper_nolock 在方法表中二分查找 SEL。找到则写入缓存返回 IMP。

▼ 一路到 NSObject 仍未找到
② 动态方法解析(Dynamic Resolution)

给类发送 +resolveInstanceMethod:+resolveClassMethod:,让类有机会用 class_addMethod 动态注册方法。本文重点

▼ 类未处理或注册失败
③ 消息转发(Message Forwarding)

依次尝试 forwardingTargetForSelector:(快速转发)、methodSignatureForSelector: + forwardInvocation:(完整转发)。仍失败则 doesNotRecognizeSelector: 抛异常。

二、完整调用链路

objc_msgSend(receiver, sel, ...) // 汇编,先查 cache_t
  ↓ cache miss
lookUpImpOrForward(...) // C 慢路径入口,加 runtimeLock
  ↓ 沿继承链查找全部失败
  ↓ 且 behavior 含 LOOKUP_RESOLVER 位
resolveMethod_locked(inst, sel, cls, behavior) // ← 入口
  ↓ runtimeLock.unlock() 解锁
  ↓ 区分 cls 是否元类
resolveInstanceMethod / resolveClassMethod // ← 实际发消息
  ↓ objc_msgSend(cls, @selector(resolveInstanceMethod:), sel)
  ↓ 用户代码可能调用 class_addMethod
lookUpImpOrForwardTryCache(...) // 重新查找,behavior 已去 RESOLVER 位
  ↓ 命中 → 返回新 IMP / 未命中 → forward_imp

三、resolveMethod_locked 关键设计

3.1 解锁:避免重入死锁

static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior) {
    lockdebug::assert_locked(&runtimeLock.get());
    runtimeLock.unlock();   // ← 关键:发 OC 消息前必须解锁
    ...
}
为何必须解锁? 接下来要给类发 +resolveInstanceMethod:,用户的实现里通常会调用 class_addMethod,而 class_addMethod 内部要重新加 runtimeLock。若不解锁则自死锁。这是所有"系统库回调用户代码"场景的通用原则——锁外回调

3.2 元类 vs 普通类:双重尝试

cls 类型解析路径对应消息
普通类resolveInstanceMethod+resolveInstanceMethod:
元类(类方法)① 先 resolveClassMethod+resolveClassMethod:
② 仍失败再 resolveInstanceMethod+resolveInstanceMethod:
为何元类要试两次? 类方法本质是元类的实例方法。早期只有 resolveInstanceMethod:,后来才加 resolveClassMethod: 让语义清晰,但运行时必须兼容老写法,所以两条路径都要试。

四、resolveInstanceMethod 关键设计

4.1 预检查:类是否实现了解析方法

if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(true))) {
    return;   // 没实现就直接跳过,省掉一次完整 msgSend
}

lookUpImpOrNilTryCache试探性查询,找不到返回 nil 而非触发转发。绝大多数类不会覆写 +resolveInstanceMethod:,这一步避免了无谓的消息发送。ISA(true) 中的 true 是 arm64e 的指针认证(PAC)校验。

4.2 真正发消息:用函数指针而非 [...] 语法

BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, resolve_sel, sel);

objc_msgSend cast 成具体签名直接调用,避免 [cls performSelector:] 等语法糖带来的额外查找。这次调用是用户代码执行点,典型实现:

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(dynamicMethod)) {
        class_addMethod(self, sel, (IMP)dynamicIMP, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

4.3 验证:以实际查找结果为准

IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
// 不信任 BOOL 返回值,必须真的能查到 imp 才算解析成功
防御性思维: 开发者可能返回 YES 但忘了 class_addMethod,也可能返回 NO 但通过别的路径加上了。运行时以实际状态为准,并在 PrintResolving 开启时打印警告:"returned YES, but no new implementation was found"。

五、解析后的回流:状态转换的精妙

return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
// 这次 behavior 已去掉 LOOKUP_RESOLVER 位
情况结果
解析成功,方法已加入重查命中,写入 cache,下次走快路径
解析失败因 RESOLVER 位已清除,不再递归进入解析,直接返回 _objc_msgForward_impcache 进入转发
关键状态机: behavior 中的 LOOKUP_RESOLVER 位扮演"是否还有解析机会"的标志。第一次进入慢路径时它为 1,调用解析后清零再查。这个一次性开关是防止无限递归解析的核心。

六、设计哲学总结

动态性 vs 性能的妥协

动态解析是懒加载机制——只有真正找不到方法时才让用户介入,找到后通过 cache 固化,下次走快路径。完全动态会让查找开销大,完全静态又失去 OC 灵活性,这是平衡点。

三阶段层层兜底

常规查找 → 动态解析 → 消息转发,每层都是上层失败后的逃生口。开发者可在任意一层介入,这种分层扩展机制让 KVO、NSProxy、Method Swizzling 等高级特性得以优雅实现。

锁的精确管理

函数命名约定 _locked 后缀显式声明锁状态契约,Debug 期 assert_locked/assert_unlocked 双重保障。涉及用户回调必须解锁——这是大型 C++ 代码库管理锁状态的标准做法。

防御式编程

不信任用户的 BOOL 返回值,以实际查找结果为准;预检查类是否实现解析方法以省掉无谓 msgSend;PrintResolving 开关帮助排查"返回 YES 却没加方法"的常见 bug。每处细节都体现了对"用户代码可能出错"的防御性思维。