由 布多(budo) 发布于 2025-05-18,更新于 2025-06-28
前言
消息机制在 iOS 开发中扮演着至关重要的角色,它为开发者提供了强大的动态性和灵活性,使得代码在运行时能够根据需要进行调整和
扩展。
本文将深入探讨 iOS 消息机制的底层实现原理,从方法调用到消息转发,揭示 Runtime 如何在运行时动态查找和执行方法。通过源码分析,帮助开发者更好地理解和运用这一核心机制。
阅读本文需要你具备以下基础知识:
- 了解消息发送和 objc_msgSend 的关系;
- 熟悉 ObjC 的类和对象的底层结构;
- 熟悉类的方法列表结构;
如果你对以上知识点还不够熟悉,建议先阅读相关文章打好基础。
本文将以对象方法举例,类方法的调用流程逻辑和对象方法基本一致。
消息查找过程
在 ObjC 中,方法调用本质上是一个消息发送的过程。当我们写下 [object method]
这样的代码时,编译器会在编译期将其转换为 objc_msgSend(object, @selector(method))
的形式。这个转换过程是 ObjC 消息机制的基础,它使得我们能够在运行时动态地查找和执行方法。
要深入理解消息发送的底层实现,我们需要从 Runtime 源码入手。在 Runtime 项目中,我们可以找到 objc_msgSend
的具体实现。下面让我们来看看这个函数的核心实现:
我在 这里 维护了一个可以直接运行调试的 Runtime 项目,方便大家直接调试源码。
MSG_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) { |
通过分析 lookUpImpOrForward
函数的源码实现,我们可以看到 Runtime 在查找方法实现时采用了多层次的查找策略,主要包括以下几个步骤:
方法查找流程:
- 首先在类的方法缓存中快速查找,这是最高效的查找方式
- 缓存未命中时,会遍历类的方法列表进行查找
- 如果当前类中未找到,则沿着继承链向上查找,对每个父类重复上述两步操作
- 找到方法实现后,会通过
log_and_fill_cache
将其缓存到当前类中,以提升后续调用性能 - 如果遍历完整个继承链仍未找到,则进入方法动态解析阶段
方法动态解析:
当常规查找流程无法找到方法实现时,Runtime 会尝试通过动态方法解析机制来处理,这部分内容我们将在下一节详细讨论。
补充说明:
getMethodNoSuper_nolock
函数负责在方法列表中查找目标方法,其内部采用了二分查找(已排序)和线性查找(未排序)两种策略,以平衡查找效率和排序开销。cache_getImp
函数则通过散列表实现方法缓存的快速查找,使用 SEL 作为键值,通过哈希算法将方法选择器映射到对应的实现地址。
这些优化策略共同构成了 ObjC 高效的消息查找机制,既保证了方法调用的性能,又维持了运行时的灵活性。
消息动态解析
当常规方法查找流程(包括缓存查找、方法列表查找和父类查找)都无法找到目标方法的实现时,Runtime 会进入方法动态解析阶段,调用 resolveMethod_locked
函数尝试动态添加方法实现。这个函数的核心实现如下:
static IMP |
通过前面的源码分析,我们已经完整地了解了 Runtime 的消息查找和动态解析机制。从 objc_msgSend 的快速查找,到 lookUpImpOrForward 的慢速查找,再到 resolveMethod_locked 的动态方法解析,我们看到了 Runtime 是如何一步步尝试找到并执行目标方法的。如果这些步骤都无法找到方法实现,Runtime 就会进入最后一道防线:消息转发机制。
接下来,让我们深入探讨消息转发机制的具体实现。
消息转发机制
消息转发机制是 ObjC Runtime 中处理未实现方法的最后一道防线,它包含快速转发和完整转发两个阶段。快速转发允许对象将消息转发给其他对象处理,而完整转发则提供了更灵活的消息处理方式。虽然消息转发的核心实现是由汇编代码完成的,但通过分析 Runtime 源码和相关资料,我们可以将其核心逻辑整理为以下伪代码实现:
int __forwarding__(void *frameStackPointer, int isStret) { |
在消息转发机制中,有一个容易被忽视的重要细节:类方法其实也支持消息转发。虽然 Xcode 在代码提示时只会显示 - (id)forwardingTargetForSelector:
等实例方法的实现,但实际上 + (id)forwardingTargetForSelector:
等类方法同样可以用于消息转发。
实现类方法的消息转发非常简单:
- 将转发方法声明为类方法(使用 + 号);
- 在转发方法中使用类对象而不是实例对象。
总结
本文深入分析了 Runtime 中消息发送的核心实现,包括 objc_msgSend 的汇编实现以及 loopUpImpOrForward 函数的工作原理。但要完全理解 ObjC 的消息机制,还需要了解以下几个关键点:
- 消息查找过程:类是如何从方法列表中定位目标方法的?getMethodNoSuper_nolock 函数在其中扮演什么角色?
- 方法缓存机制:类是如何通过 cache_getImp 函数从缓存中快速获取方法实现的?
- 对象内存结构:包括 isa 指针、类指针、属性列表、方法列表等底层数据结构。
这些知识点涉及 ObjC 对象的内存布局,建议读者结合 Runtime 源码深入学习。
另外,在实际开发中,我们经常使用 respondsToSelector: 来检查对象是否实现了某个方法。但这个方法存在一个局限性:它无法检测到通过消息转发机制实现的方法。为此,我实现了一个支持消息转发检测的 respondsToSelector 方法,代码如下:
@interface NSObject (WXL) |
补充
以下内容于 2025-06-28 补充
众所周知,在 ObjC 中,方法调用都会被转换为对 objc_msgSend
函数的调用。这个函数负责在运行时确定应该调用哪个方法实现,这一过程被称为动态方法派发。例如,当我们写下 [self dynamicMethod];
时,编译器会将其转换为 objc_msgSend(self, @selector(dynamicMethod));
的形式。
但现在有一种技术可以让 ObjC 方法绕过消息机制,在编译期就确定方法的内存地址,实现类似 C 函数的直接调用。这就是 __attribute__((objc_direct))
特性,该特性于 2019-11-07 引入,详细技术细节可参考 LLVM 代码审查。
当使用 __attribute__((objc_direct))
修饰方法时,编译器会将方法实现转换为普通的 C 函数,将方法调用转换为对这个 C 函数的调用,从而避免运行时的方法查找开销。例如 - (void)directMethod __attribute__((objc_direct)) {}
会被转换成这样:void directMethod(id self, SEL _cmd) {}
,调用处会转换成这样:directMethod(self, @selector(directMethod));
。
看到这里你可能已经迫不及待地想用 __attribute__((objc_direct))
来优化项目中的方法调用了,认为它能带来显著的性能提升。但实际情况是,在大多数业务场景下,这么做并不会带来明显的性能改善。主要原因有以下几点:
- 得益于 Apple 对
objc_msgSend
的深度优化、方法缓存机制以及现代处理器的强大性能,消息发送的开销已经非常低。 - 在典型的业务代码中,单个方法的调用频率往往不会达到需要优化的程度,因此性能提升的绝对值很小。而对于频繁调用的方法,Runtime 的方法缓存机制已经能够提供接近直接调用的性能。
更重要的是,过度使用 __attribute__((objc_direct))
会带来一系列设计上的限制:
- 继承链的破坏:无法对继承的方法使用此特性,因为它会破坏面向对象的继承关系;
- 多态性的丧失:公开方法使用此特性后,子类将无法重写该方法,失去了多态性的优势;
- 调试与维护困难:绕过了正常的消息机制,可能影响调试工具的正常工作,增加问题排查的复杂度;
- 兼容性问题:可能与其它依赖 Runtime 特性的框架产生冲突;
__attribute__((objc_direct))
特性主要具有两个核心优势:
- 方法可见性隐藏:被修饰的方法仅在当前模块内可见,不会出现在 ObjC 运行时中,甚至无法通过 Runtime API 进行查找或调用。这一特性在需要隐藏内部实现细节时非常有用;
- 二进制文件大小优化:根据相关数据,未使用的 ObjC 元数据在编译后的二进制文件
__text
段中可能占据 5~10% 的比重,使用此特性可以有效减少这部分开销。
需要注意的是,虽然隐藏可见性可以防止 hook 和私有 API 的使用,但这并非其主要设计目的。据作者描述,该特性的主要价值在于减小二进制文件体积。
个人建议仅在以下特定场景中谨慎使用此特性:
- 内部工具方法,需要隐藏实现细节;
- 模块内部需要频繁调用的辅助方法;
关于性能优化的思考:在 iOS 项目中,甚至是大部分项目中,性能往往不是最重要的考量因素。在用户体验可接受的前提下,代码的可读性、可维护性和可扩展性更为重要。
举个例子:假设某个方法调用从 10ms 优化到 1ms,看似获得了 10 倍的性能提升,但对用户而言,10ms 和 1ms 的差异在感知上几乎无法察觉。除非该方法被调用成千上万次,否则这种优化带来的实际价值微乎其微。
在业务开发中,单个方法的调用频率通常不会达到需要优化的程度。因此,与其将精力投入到微小的性能优化上,不如专注于提升代码质量和架构设计。
__attribute__((objc_direct))
虽然看起来能够通过绕过消息机制带来显著的性能提升,但实际上其带来的设计限制和潜在问题可能远超收益。在未充分理解技术细节和权衡利弊的情况下,不建议盲目大量使用此特性。