消息转发流程的源码探究

2021-03-10 14:17:35 浏览数 (1)

注:在阅读本文之前,请先阅读我之前写的如下三篇文章:

  • Runtime——消息转发流程
  • Effective Objective-C 2.0——理解消息转发机制
  • Runtime再理解

在上篇文章方法的查找流程——慢速查找中,在lookUpImpOrForward函数里面会进行方法的查找,如果最终没有找到,那么就会进入消息的转发流程,如下:

对象方法的动态方法决议

我们就从_class_resolveMethod函数开始研究,首先看其源码:

代码语言:javascript复制
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]

        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}

如果所查找的方法是实例方法,那么就调用_class_resolveInstanceMethod;如果所查找的方法是类方法,那么就调用_class_resolveClassMethod

我们先来看_class_resolveInstanceMethod:

代码语言:javascript复制
static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    //  resolveInstanceMethod adds to self a.k.a. cls
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? ' ' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE:  [%s resolveInstanceMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? ' ' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}

这里的主线就是调用OC中的 resolveInstanceMethod 方法。

上面代码中的第3~8行是一个容错判断,当找不到 resolveInstanceMethod 方法的实现的时候会走到这个判断里面来。但实际上,正常情况下所有继承自NSObject的类中,这个判断都是走不到的,因为 resolveInstanceMethod 方法是有默认实现的:

第10~11行的作用是,给cls发送一个名为SEL_resolveInstanceMethod(即resolveInstanceMethod)的消息,并且携带参数sel,这里携带的参数sel就是 resolveInstanceMethod 方法后面携带的参数。

需要注意的是,这里的cls是类对象这里对 resolveInstanceMethod 方法的查找是一个缓存快速查找的过程,因为在前面的第3行代码处已经查找过一次了

第13~16行,其作用是在cls中再次查找sel所对应的IMP。为啥需要再一次进行查找呢?不是没找到才走到这里的吗?原因就在于,我在第10~11行的时候,调用了OC中的 resolveInstanceMethod 方法,程序员可以在该方法中做相关操作。基于前面的分析,程序员需要在 resolveInstanceMethod 方法中做的操作是:给没有找到IMP的SEL动态添加一个IMP

类方法的动态方法决议

通过上面_class_resolveMethod函数的源码我们知道,如果所查找的方法是实例方法,那么就调用_class_resolveInstanceMethod;如果所查找的方法是类方法,那么就调用_class_resolveClassMethod

接下来我们就先来看看_class_resolveClassMethod的源码:

代码语言:javascript复制
static void _class_resolveClassMethod(Class cls, SEL sel, id inst)
{
    assert(cls->isMetaClass());

    if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(_class_getNonMetaClass(cls, inst), 
                        SEL_resolveClassMethod, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    //  resolveClassMethod adds to self->ISA() a.k.a. cls
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? ' ' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE:  [%s resolveClassMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? ' ' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}

第5~10行是容错处理。当找不到 resolveClassMethod 方法的实现的时候会走到这个判断里面来。但实际上,正常情况下所有继承自NSObject的类中,这个判断都是走不到的,因为 resolveClassMethod 方法是有默认实现的。

通过_class_resolveMethod函数的源码我们发现,当查找的是类方法的时候,在调用完函数之后,会再查找一遍对应的类方法,如果没有找到的话就会接着执行_class_resolveInstanceMethod函数。这是为什么呢?

查找一个类方法没找到的时候,会进入类方法的动态方法决议,也就是调用_class_resolveClassMethod函数,此时我们如果在OC的 resolveClassMethod方法里面采取了对应操作并返回YES,那么消息转发到此结束;

如果我们在OC的 resolveClassMethod方法里面没有采取对应操作,那么程序会通过lookUpImpOrNil函数检测到还没有找到对应的方法实现,因此就会调用_class_resolveInstanceMethod函数进入实例方法的动态决议,测试的参数cls是元类,因此最终会在根元类的父类NSObject中去查找OC的 resolveInstanceMethod方法

实例方法的动态决议只有一个 resolveInstanceMethod;类方法的动态决议,除了有一个 resolveClassMethod之外,还会走一遍实例方法动态决议 resolveInstanceMethod,其根本原因就是:要与【所有方法最终都要到NSObject的实例方法中去查找】这一点相呼应。

正因为当找不到对应的方法实现的时候,最终都会走到NSObject的 resolveInstanceMethod方法,因此我们就可以统一在NSObject的 resolveInstanceMethod 方法里面做处理。但是,我们不会这样去做,原因如下:

  1. 你如果要在NSObject里面做处理,那么势必会针对每个sel都做一次判断,这样的话,全工程中的判断都会集中在这里,一来代码太臃肿;二来耦合度很高,因为所有需要处理的地方都依赖这里。
  2. 如果其他人在对应的子类中也实现了resolveMethod方法,那么就不会走到NSObject的 resolveInstanceMethod 方法里面,那么这里面所做的处理就不会生效。基于此,在NSObject的 resolveInstanceMethod 方法里面做处理就很鸡肋

消息的快速转发流程

如果在动态方法决议中没有做任何的处理,那么就会走到这里的快速消息转发流程。

代码语言:javascript复制
- (id)forwardingTargetForSelector:(SEL)aSelector

在该方法里,系统给了将这个SEL转发给其他对象的机会。

其返回参数是一个对象,如果这个对象非nil、非self的话,系统会将运行的消息转发给这个对象执行。否则,会进入下面的消息慢速转发流程。

消息的慢速转发流程

当对象接收到某个消息之后,首先会去查找是否有该实现函数,如果有,那么就直接调用;如果没有,则进入消息转发流程。

首先会动态方法决议,给你自己或者你的父类提供一个内部特殊处理没有找到IMP的SEL的机会,如果你不处理,那么就进入消息快速转发流程

消息快速转发,也就是将消息转发给别的对象,如果我不将消息转发给别的对象,那么就会进入到现在所讲的慢速消息转发流程

慢速消息转发 的第一步就是你得告诉系统当前没有找到IMP的SEL的签名是什么,如下:

慢速消息转发 的第二步就是,你可以在下面的方法中对没有找到IMP的SEL进行相关的处理

- (void)forwardInvocation:(NSInvocation *)anInvocation { //打印一下无法处理的方法名 NSLog(@"forwardInvocation, 无法处理的selector:%@", NSStringFromSelector(anInvocation.selector)); //如果SecondTextClass中能够响应该SEL,那么就让SecondTextClass中相应的SEL去执行该方法 SecondTextClass *secondTextObject = [[SecondTextClass alloc] init]; SEL sel = anInvocation.selector; if ([secondTextObject respondsToSelector:sel]) { [anInvocation invokeWithTarget:secondTextObject]; } //不再采取其他的操作的话,就代表不对找不到实现的SEL进行响应,这样程序就不会崩溃了。 }

可以将forwardInvocation函数理解成是一个秘书,它会随时准备响应老板的工作,有任务派给它它就会去处理任务,没有任务的话它就待命。这个函数还有一个作用就是它会拦截所有找不到IMP的SEL要么处理它要么让它失效,总之,不会让它走到崩溃的那一步。

需要注意的是,是否进行慢速消息转发流程的关键是,是否返回对应SEL的签名,只有返回了签名,才会进行慢速转发流程,否则会进入接下来的流程(即报崩溃)。

汇编分析

实际上,上面的分析是开了上帝视角的。如果不开上帝视角,那么该如何研究呢?其中一个角度就是汇编。

这里的Norman是一个自定义的类,里面声明了play方法,但是该方法并没有实现,此时运行,就会发生崩溃,崩溃信息如上图?。

然后我们分析左边的崩溃堆栈。4是当前定位到的位置,也就是[norman play];代码调用的位置,1是发送崩溃信息的位置,2【#2 0x000000010ac7e7bc in ___forwarding___ ()】就是消息转发的位置了,我们将光标点击到2,如下:

这里应该就是消息转发的流程了,然后我们往上翻,就会依次看到熟悉的字眼:

因此,我们就可以断定,这个汇编文件就是消息转发的研究对象,然后我就翻到最顶部,看看这个汇编文件叫啥名:

我们发现,它是CoreFoundation可执行文件中的___forwarding___。

所以我们就研究这里就可以了。

反汇编分析

但是毕竟我们很多人对汇编语言不是很熟悉,就单纯能去这么干巴巴看汇编代码的话,实际上很费劲,然后我就想,是不是有一些方法可以简化这里的研究,答案就是反汇编。

所谓的反汇编,实际上就是汇编的伪代码,这里给大家推荐两个反汇编的工具,一个叫【IDA Pro】,一个叫【Hopper Disassembler

反汇编的对象就是可执行文件,接下来我们就去找CoreFoundation可执行文件。该可执行文件肯定是在系统包里面,因此,我们就需要到系统包里面去寻找。

在应用程序中找到Xcode,然后显示包内容:

然后按照如下路径找到iOS.simruntime:

然后显示iOS.simruntime的包内容,并按下面路径继续查找,最终找到CoreFoundation可执行文件:

然后我们就可以通过IDA或者Hopper来对该可执行文件进行反汇编分析了。

然后搜索【___forwarding___】就可以找到对应的反汇编伪代码了。

分析的时候可以对照汇编源码进行分析:

还是那句话,知识点不是最重要的,重要的是研究的方法和思路通过反汇编进行底层探索是进阶高级必须掌握的技能。

以上。

0 人点赞