普通视图

发现新文章,点击刷新页面。
昨天以前首页

iOS 如何找到那个方法?消息机制底层探秘

作者 布多
2025年5月18日 15:43

前言

消息机制在 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 的汇编实现,我对其进行了精简,并添加了注释。从源码中我们可以总结出消息查找的基本流程:

  1. 首先对消息接收者进行 nil 检查,如果接收者为 nil,则调用 LNilOrTagged 函数直接返回 nil,避免后续无意义的查找;
  2. 通过对象的 isa 指针获取其类对象,这是查找方法实现的第一步,因为方法实现都存储在类对象中;
  3. 在类对象的方法缓存列表中查找目标方法,如果命中缓存则直接调用方法实现,否则调用 __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 在查找方法实现时采用了多层次的查找策略,主要包括以下几个步骤:

  1. 方法查找流程:

    • 首先在类的方法缓存中快速查找,这是最高效的查找方式
    • 缓存未命中时,会遍历类的方法列表进行查找
    • 如果当前类中未找到,则沿着继承链向上查找,对每个父类重复上述两步操作
    • 找到方法实现后,会通过 log_and_fill_cache 将其缓存到当前类中,以提升后续调用性能
    • 如果遍历完整个继承链仍未找到,则进入方法动态解析阶段
  2. 方法动态解析: 当常规查找流程无法找到方法实现时,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: 等类方法同样可以用于消息转发。

实现类方法的消息转发非常简单:

  1. 将转发方法声明为类方法(使用 + 号);
  2. 在转发方法中使用类对象而不是实例对象。

总结

本文深入分析了 Runtime 中消息发送的核心实现,包括 objc_msgSend 的汇编实现以及 loopUpImpOrForward 函数的工作原理。但要完全理解 ObjC 的消息机制,还需要了解以下几个关键点:

  1. 消息查找过程:类是如何从方法列表中定位目标方法的?getMethodNoSuper_nolock 函数在其中扮演什么角色?
  2. 方法缓存机制:类是如何通过 cache_getImp 函数从缓存中快速获取方法实现的?
  3. 对象内存结构:包括 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
❌
❌