iOS 如何找到那个方法?消息机制底层探秘
前言
消息机制在 iOS 开发中扮演着至关重要的角色,它为开发者提供了强大的动态性和灵活性,使得代码在运行时能够根据需要进行调整和 扩展。
本文将深入探讨 iOS 消息机制的底层实现原理,从方法调用到消息转发,揭示 Runtime 如何在运行时动态查找和执行方法。通过源码分析,帮助开发者更好地理解和运用这一核心机制。
阅读本文需要你具备以下基础知识:
- 了解消息发送和 objc_msgSend 的关系;
- 熟悉 ObjC 的类和对象的底层结构;
- 熟悉类的方法列表结构;
如果你对以上知识点还不够熟悉,建议先阅读相关文章打好基础。
本文将以对象方法举例,类方法的调用流程逻辑和对象方法基本一致。
消息查找过程
在 ObjC 中,方法调用本质上是一个消息发送的过程。当我们写下 [object method]
这样的代码时,编译器会在编译期将其转换为 objc_msgSend(object, @selector(method))
的形式。这个转换过程是 ObjC 消息机制的基础,它使得我们能够在运行时动态地查找和执行方法。
要深入理解消息发送的底层实现,我们需要从 Runtime 源码入手。在 Runtime 项目中,我们可以找到 objc_msgSend
的具体实现。下面让我们来看看这个函数的核心实现:
我在 这里 维护了一个可以直接运行调试的 Runtime 项目,方便大家直接调试源码。
MSG_ENTRY _objc_msgSend
cmpp0, #0
// 检查接收者是否为 nil,如果是的话就执行 LNilOrTagged,其内部会返回 nil。
b.leLNilOrTagged
ldrp14, [x0]
// 获取对象的 isa 指针。
GetClassFromIsa_p16 p14, 1, x0
LGetIsaDone:
// 查找方法缓存,如果找到了就调用,如果没找到就调用 __objc_msgSend_uncached。
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
END_ENTRY _objc_msgSend
以上是 objc_msgSend
的汇编实现,我对其进行了精简,并添加了注释。从源码中我们可以总结出消息查找的基本流程:
- 首先对消息接收者进行 nil 检查,如果接收者为 nil,则调用 LNilOrTagged 函数直接返回 nil,避免后续无意义的查找;
- 通过对象的 isa 指针获取其类对象,这是查找方法实现的第一步,因为方法实现都存储在类对象中;
- 在类对象的方法缓存列表中查找目标方法,如果命中缓存则直接调用方法实现,否则调用
__objc_msgSend_uncached
进入慢速查找。
在慢速查找流程中,系统会调用 lookUpImpOrForward
函数进行更深入的方法查找(由于篇幅原因,我没有展开所有代码的调用细节,感兴趣的同学可以自行阅读源码)。以下是精简后的 lookUpImpOrForward 源码实现:
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior) {
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
IMP imp = nil;
Class curClass;
if (!cls->isInitialized()) {
behavior |= LOOKUP_NOCACHE;
}
checkIsKnownClass(cls);
cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
curClass = cls;
if (!cls || !cls->ISA()) {
imp = _objc_returnNil;
goto done;
}
for (unsigned attempts = unreasonableClassCount();;) {
// 从类的方法列表中查找方法实现。
method_t *meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
// 找到了方法实现并调用。
imp = meth->imp(false);
goto done;
}
// 获取 curClass 的父类,如果父类为 nil 的话,就跳出循环。
if ((curClass = curClass->getSuperclass()) == nil) {
imp = forward_imp;
break;
}
// 检查继承链是否存在循环情况。
if (--attempts == 0) {
_objc_fatal("Memory corruption in class list.");
}
// 从父类的方法缓存列表中查找方法实现。
imp = cache_getImp(curClass, sel);
if (imp == forward_imp) {
break;
}
if (imp) {
// 从父类的方法缓存列表中找到了方法实现
goto done;
}
/* 如果从方法缓存列表中未找到方法实现,
则回到循环起点,从父类的方法列表中查找方法实现。*/
}
// 来到这里就说明没有找到方法实现。
// 判断是否执行过方法解析?如果未执行的话就执行方法解析。
if (behavior & LOOKUP_RESOLVER) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
done:
if ((behavior & LOOKUP_NOCACHE) == 0) {
// 将方法加入 cls(当前类) 的方法缓存列表
log_and_fill_cache(cls, imp, sel, inst, curClass);
}
if ((behavior & LOOKUP_NIL) && imp == forward_imp) {
return nil;
}
return imp;
}
通过分析 lookUpImpOrForward
函数的源码实现,我们可以看到 Runtime 在查找方法实现时采用了多层次的查找策略,主要包括以下几个步骤:
-
方法查找流程:
- 首先在类的方法缓存中快速查找,这是最高效的查找方式
- 缓存未命中时,会遍历类的方法列表进行查找
- 如果当前类中未找到,则沿着继承链向上查找,对每个父类重复上述两步操作
- 找到方法实现后,会通过
log_and_fill_cache
将其缓存到当前类中,以提升后续调用性能 - 如果遍历完整个继承链仍未找到,则进入方法动态解析阶段
-
方法动态解析: 当常规查找流程无法找到方法实现时,Runtime 会尝试通过动态方法解析机制来处理,这部分内容我们将在下一节详细讨论。
补充说明:
-
getMethodNoSuper_nolock
函数负责在方法列表中查找目标方法,其内部采用了二分查找(已排序)和线性查找(未排序)两种策略,以平衡查找效率和排序开销。 -
cache_getImp
函数则通过散列表实现方法缓存的快速查找,使用 SEL 作为键值,通过哈希算法将方法选择器映射到对应的实现地址。
这些优化策略共同构成了 ObjC 高效的消息查找机制,既保证了方法调用的性能,又维持了运行时的灵活性。
消息动态解析
当常规方法查找流程(包括缓存查找、方法列表查找和父类查找)都无法找到目标方法的实现时,Runtime 会进入方法动态解析阶段,调用 resolveMethod_locked
函数尝试动态添加方法实现。这个函数的核心实现如下:
static IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior) {
// 执行对象方法解析逻辑。
if (!cls->isMetaClass()) {
// 这行代码等同于:[cls resolveInstanceMethod:sel]
resolveInstanceMethod(inst, sel, cls);
} else {// 执行类方法解析逻辑。
// 这行代码等同于:[cls resolveClassMethod:sel]
resolveClassMethod(inst, sel, cls);
}
// 函数内部最终会调用 lookUpImpOrForward 函数。
return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
通过前面的源码分析,我们已经完整地了解了 Runtime 的消息查找和动态解析机制。从 objc_msgSend 的快速查找,到 lookUpImpOrForward 的慢速查找,再到 resolveMethod_locked 的动态方法解析,我们看到了 Runtime 是如何一步步尝试找到并执行目标方法的。如果这些步骤都无法找到方法实现,Runtime 就会进入最后一道防线:消息转发机制。
接下来,让我们深入探讨消息转发机制的具体实现。
消息转发机制
消息转发机制是 ObjC Runtime 中处理未实现方法的最后一道防线,它包含快速转发和完整转发两个阶段。快速转发允许对象将消息转发给其他对象处理,而完整转发则提供了更灵活的消息处理方式。虽然消息转发的核心实现是由汇编代码完成的,但通过分析 Runtime 源码和相关资料,我们可以将其核心逻辑整理为以下伪代码实现:
int __forwarding__(void *frameStackPointer, int isStret) {
id receiver = *(id *)frameStackPointer;
SEL sel = *(SEL *)(frameStackPointer + 8);
Class receiverClass = object_getClass(receiver);
// 调用 forwardingTargetForSelector: 方法。
if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) {
id forwardingTarget = [receiver forwardingTargetForSelector:sel];
if (forwardingTarget && forwardingTarget != receiver) {
return objc_msgSend(forwardingTarget, sel, ...);
}
}
// 调用 methodSignatureForSelector 获取方法签名后再调用 forwardInvocation。
if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) {
NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel];
if (methodSignature) {
if (class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) {
NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature frame:frameStackPointer];
[receiver forwardInvocation:invocation];
void *returnValue = NULL;
[invocation getReturnValue:&returnValue];
return returnValue;
}
}
}
// 如果以上两个方法都没有处理消息,则调用 doesNotRecognizeSelector 方法。
if (class_respondsToSelector(receiverClass,@selector(doesNotRecognizeSelector:))) {
[receiver doesNotRecognizeSelector:sel];
}
kill(getpid(), 9);
}
在消息转发机制中,有一个容易被忽视的重要细节:类方法其实也支持消息转发。虽然 Xcode 在代码提示时只会显示 - (id)forwardingTargetForSelector:
等实例方法的实现,但实际上 + (id)forwardingTargetForSelector:
等类方法同样可以用于消息转发。
实现类方法的消息转发非常简单:
- 将转发方法声明为类方法(使用 + 号);
- 在转发方法中使用类对象而不是实例对象。
总结
本文深入分析了 Runtime 中消息发送的核心实现,包括 objc_msgSend 的汇编实现以及 loopUpImpOrForward 函数的工作原理。但要完全理解 ObjC 的消息机制,还需要了解以下几个关键点:
- 消息查找过程:类是如何从方法列表中定位目标方法的?getMethodNoSuper_nolock 函数在其中扮演什么角色?
- 方法缓存机制:类是如何通过 cache_getImp 函数从缓存中快速获取方法实现的?
- 对象内存结构:包括 isa 指针、类指针、属性列表、方法列表等底层数据结构。
这些知识点涉及 ObjC 对象的内存布局,建议读者结合 Runtime 源码深入学习。
另外,在实际开发中,我们经常使用 respondsToSelector: 来检查对象是否实现了某个方法。但这个方法存在一个局限性:它无法检测到通过消息转发机制实现的方法。为此,我实现了一个支持消息转发检测的 respondsToSelector 方法,代码如下:
@interface NSObject (WXL)
- (BOOL)wxl_respondsToSelectorIncludingForwarding:(SEL)aSel;
@end
@implementation NSObject (WXL)
- (BOOL)wxl_respondsToSelectorIncludingForwarding:(SEL)aSel {
if ([self respondsToSelector:aSel]) {
return YES;
}
// 检查消息转发是否能处理消息。
if ([self respondsToSelector:@selector(forwardingTargetForSelector:)]) {
id forwardingTarget = [self forwardingTargetForSelector:aSel];
if (forwardingTarget && forwardingTarget != self) {
return YES;
}
}
if ([self respondsToSelector:@selector(methodSignatureForSelector:)]) {
NSMethodSignature *signature = [self methodSignatureForSelector:aSel];
if (signature && [self respondsToSelector:@selector(forwardInvocation:)]) {
return YES;
}
}
return NO;
}
@end