面试题答案
一键面试SEL在方法缓存中的工作原理
缓存的建立
- 方法的注册:当一个类被加载到内存时,类的元数据会被初始化。其中包括类的方法列表,每个方法都有一个与之关联的选择器(SEL)。SEL本质上是一个指向方法名称的唯一标识符(通常是一个字符串的哈希值)。
- 缓存的初始化:每个类对象都有一个方法缓存(
cache_t
),用于存储经常调用的方法。当类的实例第一次调用某个方法时,该方法会被添加到缓存中。缓存结构通常是一个哈希表,以SEL作为键,以对应的IMP(函数指针,指向方法的实现)作为值。 - 缓存插入:当一个方法被调用时,运行时系统首先计算方法选择器的哈希值。然后根据哈希值在缓存哈希表中查找合适的位置。如果该位置为空,则直接将SEL和IMP插入到缓存中;如果该位置已被占用(哈希冲突),则通过链表等方式解决冲突,将新的SEL - IMP对添加到链表中。
查找过程
- 缓存查找:当一个对象接收到一条消息(调用一个方法)时,运行时系统首先根据对象的类获取其方法缓存。然后,计算消息对应的SEL的哈希值,通过哈希值在缓存哈希表中查找对应的IMP。如果在缓存中找到匹配的SEL,则直接返回对应的IMP并执行方法,这大大提高了方法调用的速度。
- 快速查找优化:缓存查找过程通常经过优化,以尽可能快地定位到目标方法。例如,缓存哈希表的设计会尽量减少哈希冲突,并且查找操作可以在常数时间(平均情况下)内完成。
缓存未命中时的处理流程
- 方法列表查找:如果在缓存中未找到对应的SEL,运行时系统会在类的方法列表中进行线性查找。类的方法列表包含了该类定义的所有实例方法。系统会逐个比较方法列表中的SEL与目标SEL,直到找到匹配的方法或者遍历完整个列表。
- 父类查找:如果在当前类的方法列表中未找到匹配的方法,运行时系统会沿着继承链向上查找父类的方法列表。这个过程会递归进行,直到找到匹配的方法或者到达根类(通常是
NSObject
)。如果在根类中也未找到匹配的方法,则进入动态方法解析阶段。 - 动态方法解析:在动态方法解析阶段,运行时系统会给程序一次机会,通过
+resolveInstanceMethod:
(针对实例方法)或+resolveClassMethod:
(针对类方法)方法动态添加方法实现。如果动态方法解析成功添加了方法实现,则缓存该方法并执行;否则,进入消息转发阶段。 - 消息转发:如果动态方法解析没有成功,运行时系统会进入完整的消息转发流程。首先是备用接收者(
-forwardingTargetForSelector:
)阶段,尝试找到其他可以处理该消息的对象;如果没有找到备用接收者,则进入完整转发(-methodSignatureForSelector:
和-forwardInvocation:
)阶段,在这个阶段,程序可以构建一个NSInvocation
对象,将消息转发给其他对象处理。如果最终还是无法处理该消息,系统会抛出NSInvalidArgumentException
异常。
这种机制对提高程序性能的意义
- 减少方法查找时间:方法缓存机制避免了每次方法调用都在整个类的方法列表或继承链中进行线性查找。通过缓存经常调用的方法,运行时系统可以在常数时间内(平均情况下)找到方法的实现,大大提高了方法调用的效率,尤其是对于频繁调用的方法。
- 优化内存访问:缓存位于对象的类结构附近,访问缓存的内存局部性较好,减少了内存访问的开销。相比在整个继承链中查找方法,缓存查找的内存访问模式更高效,有助于提高CPU缓存命中率,进一步提升性能。
- 提高整体运行效率:方法缓存机制使得程序在运行时能够快速响应方法调用,减少了因方法查找带来的延迟,从而提高了整个应用程序的响应速度和运行效率,特别是在处理复杂业务逻辑和频繁方法调用的场景下,性能提升尤为显著。