由 布多(budo) 发布于 2025-05-18
前言
消息机制在 iOS 开发中扮演着至关重要的角色,它为开发者提供了强大的动态性和灵活性,使得代码在运行时能够根据需要进行调整和
扩展。
本文将深入探讨 iOS 消息机制的底层实现原理,从方法调用到消息转发,揭示 Runtime 如何在运行时动态查找和执行方法。通过源码分析,帮助开发者更好地理解和运用这一核心机制。
阅读本文需要你具备以下基础知识:
- 了解消息发送和 objc_msgSend 的关系;
- 熟悉 ObjC 的类和对象的底层结构;
- 熟悉类的方法列表结构;
如果你对以上知识点还不够熟悉,建议先阅读相关文章打好基础。
本文将以对象方法举例,类方法的调用流程逻辑和对象方法基本一致。
消息查找过程
在 ObjC 中,方法调用本质上是一个消息发送的过程。当我们写下 [object method]
这样的代码时,编译器会在编译期将其转换为 objc_msgSend(object, @selector(method))
的形式。这个转换过程是 ObjC 消息机制的基础,它使得我们能够在运行时动态地查找和执行方法。
要深入理解消息发送的底层实现,我们需要从 Runtime 源码入手。在 Runtime 项目中,我们可以找到 objc_msgSend
的具体实现。下面让我们来看看这个函数的核心实现:
我在 这里 维护了一个可以直接运行调试的 Runtime 项目,方便大家直接调试源码。
MSG_ENTRY _objc_msgSend cmp p0, #0 b.le LNilOrTagged
ldr p14, [x0] GetClassFromIsa_p16 p14, 1, x0
LGetIsaDone: 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; } 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) { 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()) { resolveInstanceMethod(inst, sel, cls); } else { resolveClassMethod(inst, sel, cls); }
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); if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) { id forwardingTarget = [receiver forwardingTargetForSelector:sel]; if (forwardingTarget && forwardingTarget != receiver) { return objc_msgSend(forwardingTarget, sel, ...); } } 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; } } } 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
|