Runtime之动态方法决议和消息转发

545 阅读7分钟

前言:前面分析过了Runtime的三个阶段中的查找方法的过程,分为了慢速查找和快速查找,在分析的时候我们还发现如果查找结束没有发现方法的时候,系统并没有直接返回失败的信息,而是中间又经历了其他的容错处理,增加了程序的健壮性和扩展性,为开发者提供了更大的操作空间!接下来就让我们继续看看具体系统做了些什么?

runtime消息发送系列文章友情链接

【一】Runtime 消息快速查找流程分析

【二】Runtime消息慢速查找流程分析

【三】Runtime之动态方法决议和消息转发

一、动态方法解析

1、 我们通过方法查找部分的代码,可以得到如下的源码:


 // No implementation found. Try method resolver once.  011   010

    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

上述方法为单例实现,表示未找到方法实现的话,就进入动态方法决议阶段

2、跟踪方法解析过程


static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertLocked();
    ASSERT(cls->isRealized());

    runtimeLock.unlock();

     #pragma mark - 如果不是类方法,就进入实例化方法处理
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        resolveInstanceMethod(inst, sel, cls);
    } 
    else {
    
     #pragma mark -如果是类方法,就进入类方法处理
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        resolveClassMethod(inst, sel, cls);
        if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }
    
     #pragma mark - 进入缓存查找
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}

看上面传入的是类方法还是实例方法去走不同的逻辑,接下来以实例方法为例,进行跟踪

3、动态方法决议过程(实例化方法为例)


static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:);
    
    #pragma mark - 如果没有进行动态方法解析,中断执行后续
    if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
        // Resolver not implemented.
        return;
    }
    
     #pragma mark - 进行了动态方法解析后,这里进行消息发送操作,执行objc_msgSend
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, resolve_sel, sel);

    //这里进行了缓存方法操作,下次就不走这里了
    IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
    ......
    //打印日志的一些操作省略......
    }
}

这里是对动态决议过程的结果处理,如果动态决议对方法进行了处理,这里重新执行objc_msgSend,也是为什么自定义类中的 + resolveInstanceMethod 方法执行了两次的原因。到这里动态决议结束,如果没有动态方法决议处理操作,就继续进行下一步,我们继续跟踪源码。

4、动态方法决议未实现的后续流程分析

为了知道程序在进行了动态方法决议之后的执行流程,我们新建一个demo工程进行分析。

image.png

image.png

我们在方法动态解析的地方添加相应的方法other,可以看到程序运行的结果:


2021-07-04 20:50:59.009403+0800 方法调用demo[1611:5853209] -[Person other]
Program ended with exit code: 0

我们不为程序添加动态解析,得到程序崩溃信息,使用lldb命令中的bt命令,得到堆栈信息:

image.png


(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGABRT
    frame #0: 0x00007fff2043f92e libsystem_kernel.dylib`__pthread_kill + 10
    frame #1: 0x000000010038de79 libsystem_pthread.dylib`pthread_kill + 263
    frame #2: 0x00007fff203c3411 libsystem_c.dylib`abort + 120
    frame #3: 0x00007fff20431ef2 libc++abi.dylib`abort_message + 241
    frame #4: 0x00007fff204235fd libc++abi.dylib`demangling_terminate_handler() + 266
    frame #5: 0x00007fff2031c58d libobjc.A.dylib`_objc_terminate() + 96
    frame #6: 0x00007fff20431307 libc++abi.dylib`std::__terminate(void (*)()) + 8
    frame #7: 0x00007fff20433beb libc++abi.dylib`__cxxabiv1::failed_throw(__cxxabiv1::__cxa_exception*) + 27
    frame #8: 0x00007fff20433bb2 libc++abi.dylib`__cxa_throw + 116
    frame #9: 0x00007fff20319ec0 libobjc.A.dylib`objc_exception_throw + 350
    frame #10: 0x00007fff2066438d CoreFoundation`-[NSObject(NSObject) doesNotRecognizeSelector:] + 132
    frame #11: 0x00007fff2054990b CoreFoundation`___forwarding___ + 1448
    frame #12: 0x00007fff205492d8 CoreFoundation`_CF_forwarding_prep_0 + 120
  * frame #13: 0x0000000100003e86 方法调用demo`main(argc=1, argv=0x00007ffeefbff468) at main.m:14:9
    frame #14: 0x00007fff20489f5d libdyld.dylib`start + 1
    frame #15: 0x00007fff20489f5d libdyld.dylib`start + 1

分析堆栈信息,从下而上的分析,我们得到 ‘CF_forwarding_prep_0’ 、 ‘__ forwarding__’ 、‘doesNotRecognizeSelector’方法。

我们通过这几个方法,可以得知,他们都是CoreFoundation库中的方法,尝试下在runtime源码中能不能找到;

image.png

image.png

最后我们通过doesNotRecognizeSelector方法找到相关源码,但是依然对分析程序没有帮助,于是我们只能通过反汇编的方式去分析CoreFoundation库(使用了工具IDA64)。

image.png

image.png

我们把搜索到的所有方法,都写入Person的实现中,然后查看方法的调用以及调用顺序,如下:

image.png

于是,我们知道了在动态消息决议之后执行的过程,下面我们就说说这样一个过程————消息转发流程!

二、消息转发

我们都知道,在执行resolveInstanceMethod没有相关处理逻辑,会进入另外一个方法:forwardingTargetForSelector,通过方法名我们知道是需要传入一个可以处理方法的对象,也就是Person类的test方法没实现,我们要传入一个其他的类去处理test方法。接下来我们就这样尝试:

1、forwardingTargetForSelector方法

image.png

这里我们发现,传入了一个实现了test方法的对象Man之后,forwardingTargetForSelector里面调用了Man的test方法,程序运行并没有崩溃,而且后面的方法都没有再走。如果我们不实现forwardingTargetForSelector,继续探索下一个方法methodSignatureForSelector。

2、methodSignatureForSelector方法

image.png

可以看到,我们在下一个方法,传入了需要的方法签名(按照声明的-(void)test;)。在执行methodSignatureForSelector方法之后,重复去调用了resolveInstanceMethod进行了消息决议,发现消息发送依然不成功,后面又去走forwardInvocation方法。

3、forwardInvocation方法

我们分析,传入了方法,不传入对象,消息发送的时候objc_msgSend就找不到消息接收者receiver,所以应该传入一个实现了test方法的类Man,我们接下来看结果:

image.png

4、doesNotRecognizeSelector方法

从调试情况分析,在各个环节都不处理的情况下,会走这个方法,同时函数会抛出异常reason: '-[xxxClass xxx]: unrecognized selector sent to instance 0x100614280'。

三、对Runtime消息发送的总结

1、forwardingTargetForSelector和methodSignatureForSelector与forwardInvocation都可以实现方法的转发过程,为什么不直接使用forwardingTargetForSelector呢?

分析和认识:虽然快速转发和慢速转发过程都能满足,而且好像forwardingTargetForSelector效率和便捷性还高点,但是为了复杂场景的需求,比如监控未实现的方法调用,崩溃的特殊处理等,在forwardInvocation中处理更方便一些!

2、消息发送的典型应用场景

比如防崩溃的处理,很多场景下,方法忘记写实现,特别是同时有很多个类的时候,我们去寻找定位,没有那么方便,通过AOP切面编程的方式,采集错误信息,可以用于日志上报等,有利于开发和维护。

3、Runtime消息发送的流程图(消息发送、动态方法决议和消息转发过程)

  • 祭上我的手工流程图(消息发送和动态方法解析) image.png
  • 祭上我的手工流程图(消息转发) image.png

以上就是关于Runtime消息发送,动态方法解析和消息转发流程的总结,后期有时间的话,还要对其做一些更新和完善,如果您喜欢的话,欢迎点赞收藏!如有问题欢迎提问,一起学习一起进步!