基于 objc4 源码 · resolveMethod_locked / resolveInstanceMethod 详解
Objective-C 的 objc_msgSend 在 cache 未命中后会进入慢路径,整个慢路径分三个阶段,每一阶段都是上一阶段失败后的"最后机会"。动态方法解析处于第二阶段。
从 cls 沿 superclass 链向上,逐层调用 getMethodNoSuper_nolock 在方法表中二分查找 SEL。找到则写入缓存返回 IMP。
给类发送 +resolveInstanceMethod: 或 +resolveClassMethod:,让类有机会用 class_addMethod 动态注册方法。本文重点。
依次尝试 forwardingTargetForSelector:(快速转发)、methodSignatureForSelector: + forwardInvocation:(完整转发)。仍失败则 doesNotRecognizeSelector: 抛异常。
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。若不解锁则自死锁。这是所有"系统库回调用户代码"场景的通用原则——锁外回调。
| cls 类型 | 解析路径 | 对应消息 |
|---|---|---|
| 普通类 | resolveInstanceMethod | +resolveInstanceMethod: |
| 元类(类方法) | ① 先 resolveClassMethod | +resolveClassMethod: |
| ② 仍失败再 resolveInstanceMethod | +resolveInstanceMethod: |
resolveInstanceMethod:,后来才加 resolveClassMethod: 让语义清晰,但运行时必须兼容老写法,所以两条路径都要试。
if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(true))) { return; // 没实现就直接跳过,省掉一次完整 msgSend }
用 lookUpImpOrNilTryCache 先试探性查询,找不到返回 nil 而非触发转发。绝大多数类不会覆写 +resolveInstanceMethod:,这一步避免了无谓的消息发送。ISA(true) 中的 true 是 arm64e 的指针认证(PAC)校验。
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]; }
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls); // 不信任 BOOL 返回值,必须真的能查到 imp 才算解析成功
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 进入转发 |
LOOKUP_RESOLVER 位扮演"是否还有解析机会"的标志。第一次进入慢路径时它为 1,调用解析后清零再查。这个一次性开关是防止无限递归解析的核心。
动态解析是懒加载机制——只有真正找不到方法时才让用户介入,找到后通过 cache 固化,下次走快路径。完全动态会让查找开销大,完全静态又失去 OC 灵活性,这是平衡点。
常规查找 → 动态解析 → 消息转发,每层都是上层失败后的逃生口。开发者可在任意一层介入,这种分层扩展机制让 KVO、NSProxy、Method Swizzling 等高级特性得以优雅实现。
函数命名约定 _locked 后缀显式声明锁状态契约,Debug 期 assert_locked/assert_unlocked 双重保障。涉及用户回调必须解锁——这是大型 C++ 代码库管理锁状态的标准做法。
不信任用户的 BOOL 返回值,以实际查找结果为准;预检查类是否实现解析方法以省掉无谓 msgSend;PrintResolving 开关帮助排查"返回 YES 却没加方法"的常见 bug。每处细节都体现了对"用户代码可能出错"的防御性思维。