美文网首页
Objective-C 动态方法决议

Objective-C 动态方法决议

作者: 冼同学 | 来源:发表于2021-07-13 17:20 被阅读0次

    前言

    上一篇文章慢速方法查找一文详细分析了消息慢速查找的流程,当在找不到的时候imp = forward_imp(消息转发),那么这篇文章主要就是探索消息转发的过程,以及我们可以在这过程中可以做出哪些灵性的处理。动态方法决议又是怎么实现的?带着问题开始我们的探索吧!!哈哈

    动态方法决议

    通过汇编的断点可以得知,当imp没有找到的时候会进入libobjc.A.dylib_objc_msgForward_impcache方法,那么上篇文章_lookUpImpOrForward慢速方法查找已经知道其中的逻辑如下:

    //动态方法决议
        if (slowpath(behavior & LOOKUP_RESOLVER)) {
            //behavior = 3   LOOKUP_RESOLVER = 2
            //3^2 = 1
            behavior ^= LOOKUP_RESOLVER;
            //动态方法决议
            return resolveMethod_locked(inst, sel, cls, behavior);
        }
    
    • behavior上篇文章已经分析,值中有LOOKUP_INITIALIZE|LOOKUP_RESOLVER进入后异或LOOKUP_INITIALIZE|LOOKUP_RESOLVER^ LOOKUP_RESOLVER = LOOKUP_INITIALIZE,相当于清空了LOOKUP_RESOLVER
    • resolveMethod_locked的最后一个参数就是LOOKUP_INITIALIZE

    resolveMethod_locked源码分析

    static NEVER_INLINE IMP
    resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
    {
        runtimeLock.assertLocked();
        ASSERT(cls->isRealized());
    
        runtimeLock.unlock();
        //cls不是元类进入以下判断
        if (! cls->isMetaClass()) {
            resolveInstanceMethod(inst, sel, cls);
        } 
        else {//这里是cls是元类
            resolveClassMethod(inst, sel, cls);
            if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
                resolveInstanceMethod(inst, sel, cls);
            }
        }
        // chances are that calling the resolver have populated the cache
        // so attempt using it
        //如果没有找到方法决议,这里又会重复查找一遍
        //为什么这样子做?那么肯定有什么地方会加入之前查找不存在的方法
        return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
    }
    
    • 当快速查找和慢速查找都没有找到需要的imp的时候,就会进入动态方法决议查找的流程,即resolveMethod_locked方法。
    • 查找的是实例方法则进行对象方法动态决议resolveInstanceMethod
    • 查找的流程会根据isa的走位来进行查找,类->元类->根元类
    • 如果都没找到,最后会调用lookUpImpOrForwardTryCache查找(重新查找一遍)。

    补充:查找的是类方法则先进行类方法动态决议resolveClassMethod,再执行resolveInstanceMethod(这里resolveInstanceMethod调用与实例方法的resolveInstanceMethod参数不同。)。

    lookUpImpOrNilTryCache源码分析

    IMP lookUpImpOrForwardTryCache(id inst, SEL sel, Class cls, int behavior)
    {
        return _lookUpImpTryCache(inst, sel, cls, behavior);
    }
    
    ALWAYS_INLINE
    static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior)
    {
        runtimeLock.assertUnlocked();
        //判断类是否已经初始化,正常情况下类是已经初始化了,所以这个判断基本不会进入
        if (slowpath(!cls->isInitialized())) {
            // see comment in lookUpImpOrForward
            //进入慢速消息查找流程,因为已经查找动态决议方法,之后behavior = LOOK_INITIALIZE
            //没有了动态决议的参数(LOOK_RESOLVER)
            return lookUpImpOrForward(inst, sel, cls, behavior);
        }
        //进行缓存快速查找
        IMP imp = cache_getImp(cls, sel);
        //缓存中查找到imp,直接进行done流程
        if (imp != NULL) goto done;
    #if CONFIG_USE_PREOPT_CACHES
        //动态共享缓存中查找
        if (fastpath(cls->cache.isConstantOptimizedCache(/* strict */true))) {
            imp = cache_getImp(cls->cache.preoptFallbackClass(), sel);
        }
    #endif
        if (slowpath(imp == NULL)) {
            //imp不存在的话,继续进行消息的慢速查找流程
            return lookUpImpOrForward(inst, sel, cls, behavior);
        }
    
    done:
        //判断消息是否已经转发,_objc_msgForward_impcache方法会讲方法写进缓存
        if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
            return nil;
        }
        //返回imp,在之后的流程处理
        return imp;
    }
    
    • isInitialized用来判断类(cls)是否已经初始化,一般情况下是不会进入的。
    • 先去查找缓存(cache)中的imp,有的话就返回imp
    • 没有在缓存(cache)中找到的话,就会尝试在共享缓存中查找,找到就返回imp
    • 仍然没有会进行lookUpImpOrForward也就是再进行一次慢速消息查找。
    • lookUpImpOrNilTryCache的主要作用通过LOOKUP_NIL来控制插入缓存,不管sel对应的imp有没有实现,还有就是如果imp返回了有值那么一定是在动态方法决议中动态实现了imp

    注意:既然这个函数也是进行快速慢速消息查找的,那么就说明resolveInstanceMethodresolveClassMethod可以在某个时机将方法加入类中(加入到cache)。这样后面方法的调用才有意义。

    对象方法动态决议 resolveInstanceMethod

    通过以上的分析,就是在快速慢速消息查找过程中找不到imp的话,苹果仍然会给机会给我们在resolveInstanceMethod方法中进行处理,那么我们可以猜想在resolveInstanceMethod会在类中添加imp,请往下看resolveInstanceMethod方法源码:

    static void resolveInstanceMethod(id inst, SEL sel, Class cls)
    {
        runtimeLock.assertUnlocked();
        ASSERT(cls->isRealized());
        SEL resolve_sel = @selector(resolveInstanceMethod:);
        //先进行元类查找,是否实现了resolveInstanceMethod实例方法,也就是类方法。
        //没有实现的话就返回,但是这里不返回,原因是NSObject默认实现了resolveInstanceMethod
        if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
            // Resolver not implemented.
            return;
        }
    
        BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
        //发送消息(resolveInstanceMethod),由于接受者是类,所以以下判断进去"+"方法
        bool resolved = msg(cls, resolve_sel, 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 imp = lookUpImpOrNilTryCache(inst, sel, cls);
    
        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));
            }
        }
    }
    
    • 首先判断cls是否为元类。
    • 先进行元类resolveInstanceMethod方法的查找,找到之后会进行缓存。
    • 系统会自动给类(cls)发送resolveInstanceMethod消息,既然是给类发送消息,那么resolveInstanceMethod是类方法。(+resolveInstanceMethod)。
    • 接着进行imp的快速和慢速的查找流程,但是resolveInstanceMethod方法没有返回imp,原因在于这里不需要返回只需要对缓存进行更新的处理。
    • lookUpImpOrNilTryCachelookUpImpOrForwardTryCache唯一区别就是是否进行动态转发,这里是不进行
    • 可以看到返回的resolved只是进行了日志打印。也就是resolved返回YES/NO对功能没有影响。

    注意:如果没有实现,缓存中没有,进入lookUpImpOrForward查找,sel没有查找到对应的imp,此时imp = forward_imp动态方法决议只调用一次,此时会走done_unlockdone流程,既selforward_imp插入缓存,进行消息转发。

    类方法resolveInstanceMethod动态决议

    static void resolveClassMethod(id inst, SEL sel, Class cls)
    {   //inst->对象  cls->类  sel->方法编号
        runtimeLock.assertUnlocked();
        ASSERT(cls->isRealized());
        ASSERT(cls->isMetaClass());
        //查询元类是否实现,NSObject默认实现了resolveClassMethod
        if (!lookUpImpOrNilTryCache(inst, @selector(resolveClassMethod:), cls)) {
            // Resolver not implemented.
            return;
        }
        //返回类
        Class nonmeta;
        {
            mutex_locker_t lock(runtimeLock);
            nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
            // +initialize path should have realized nonmeta already
            if (!nonmeta->isRealized()) {
                _objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
                            nonmeta->nameForLogging(), nonmeta);
            }
        }
        BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
        //向类中发送消息
        bool resolved = msg(nonmeta, @selector(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 = lookUpImpOrNilTryCache(inst, sel, cls);
    if (resolved  &&  PrintResolving) {
        //以下是进行一些日志的输出
            if (imp) {
                ...
            }
            else {
                // Method resolver didn't add anything?
                ...
            }
        }
    }
    
    • resolveClassMethodNSobject中已经实现,只要元类初始化就可以了,目的是缓存在元类中
    • 调用resolveClassMethod类方法,目的是实现可能resolveClassMethod方法中动态实现sel对应的imp
    • imp = lookUpImpOrNilTryCache(inst, sel, cls)缓存sel对应的imp,不管imp有没有动态添加,如果没有缓存的就是forward_imp

    resolveInstanceMethod实例探究

    创建XXPerson类,声明sayLost方法,但是不进行实现,代码如下:

    @interface XXPerson : NSObject
    -(void)sayLost;
    @end
    

    在XXperson.m中添加resolveInstanceMethod方法,并打印相关的信息,代码如下:

    #import "XXPerson.h"
    @implementation XXPerson
    +(BOOL)resolveInstanceMethod:(SEL)sel{
        NSLog(@"--xjl--%@",NSStringFromSelector(sel));
        return [super resolveInstanceMethod:sel];
    }
    @end
    

    进行sayLost调用之后发现如下:

    代码调用
    疑问:在崩溃之前确实调用了resolveInstanceMethod方法,而且调用了2次,这是为什么呢?众所周知,第一次系统会走动态方法决议,NSObject调用resolveInstanceMethod,那么第二次呢?

    打开函数调用栈查看情况如下:(注意栈是先进后出的)

    第一次进入
    • 第一次进入resolveInstanceMethod时查看堆栈信息,发现走的是慢速查找流程的动态决议方法。
      第二次进入
    • 由上图可知,第二次调用resolveInstanceMethod是由系统库coreFoundation调起的。在消息转发完成之后再次开启了慢速查找流程,进入动态方法决议又调用了一次resolveInstanceMethod,所以总共调用了两次,第二次调用的详细流程会在后面详细分析。

    动态添加sayLost方法

    动态添加方法
    • 当第一次进来resolveInstanceMethod方法的时候,我们动态添加了sayLost方法,lookUpImpOrForwardTryCache直接获取imp,直接调用imp,查找流程结束。
    • 动态方法协议成功之后程序崩溃情况也得到了解决,这是系统给开发者容错的机会。

    具体流程:resolveMethod_locked--> resolveInstanceMethod --> 调用resolveInstanceMethod --> lookUpImpOrNilTryCache(inst, sel, cls) --> lookUpImpOrForwardTryCache --> 调用imp

    resolveClassMethod实例探究

    新创建XJLPerson类,并在类中定义类方法+(void)test,在XJLPerson.m文件中实现+resolveClassMethod方法,代码如下:

    @interface XJLPerson : NSObject
    +(void)test;
    @end
    
    #import "XJLPerson.h"
    @implementation XJLPerson
    +(BOOL)resolveClassMethod:(SEL)sel{
        NSLog(@"--xjl--%@",NSStringFromSelector(sel));
        return  [super resolveClassMethod:sel];
    }
    @end
    

    调用结果:

    2021-07-13 16:18:02.437689+0800 KCObjcBuild[16765:324532] --xjl--test
    2021-07-13 16:18:02.438672+0800 KCObjcBuild[16765:324532] --xjl--test
    2021-07-13 16:18:02.439058+0800 KCObjcBuild[16765:324532] +[XJLPerson test]: unrecognized selector sent to class 0x1000086b0
    
    • resolveClassMethod方法也像resolveInstanceMethod方法一样,调用了两次,而且逻辑都是一样的。
    • 调用resolveClassMethod以后,会去查找lookUpImpOrNilTryCache有没有具体动态实现sel对应的imp,元类的缓存中此时有sel对应的imp,这个impforward_implookUpImpOrNilTryCache里面有判断直接返回nil,此时直接到resolveInstanceMethod查找,因为类方法实际上就是元类中的实例方
    • 如果最后还是没有实现lookUpImpOrForwardTryCache获取到forward_imp进入消息转发流程。

    动态添加+test方法

    动态添加类方法
    • resolveClassMethod只调用一次,因为动态添加了test方法
    • resolveClassMethodresolveInstanceMethod的调用流程基本一样,如果resolveClassMethod没有查询到调用一次resolveInstanceMethod调用。

    resolveClassMethod方法拓展

    @interface XJLPerson : NSObject
    +(void)test;
    @end
    
    #import "XJLPerson.h"
    @implementation XJLPerson
    +(BOOL)resolveClassMethod:(SEL)sel{
        NSLog(@"resolveClassMethod--xjl--%@",NSStringFromSelector(sel));
        return  [super resolveClassMethod:sel];
    }
    +(BOOL)resolveInstanceMethod:(SEL)sel{
        NSLog(@"resolveInstanceMethod--xjl--%@",NSStringFromSelector(sel));
        return  [super resolveClassMethod:sel];
    }
    @end
    

    打印结果如下:

    2021-07-13 17:04:29.162523+0800 KCObjcBuild[17860:344756] resolveClassMethod--xjl--test
    2021-07-13 17:04:29.163433+0800 KCObjcBuild[17860:344756] resolveClassMethod--xjl--test
    

    这就奇怪了,进入了两次resolveClassMethod方法,完全没有进入resolveInstanceMethod方法啊。不是说了类方法就是元类的实例方法嘛?别急,我们先进去源码断点看看是啥回事咯!!!别慌!

    查询元类的缓存
    通过上面lldb调试发现instXJLPerson类,其sa指向元类与cls的地址一致,那么cls就是XJLPerson的元类。
    断点进入resolveClassMethod方法中调用的lookUpImpOrNilTryCache方法:
    断点进入lookUpImpOrNilTryCache方法
    此时instXJLPerson的元类,cls是根元类。快速和慢速查找实到根元类查找,意味着元类调用了实例方法
    msg方法
    msg(cls,resolve_sel,sel)也可以验证objc_msgSend是发送消息是不区分-+方法的。objc_msgSend的接收者cls是元类,这意味着向元类中发消息,消息的查找会到元类的元类(根元类)中查找,所以resolveInstanceMethod在元类中,在类中是不被调用的。虽然类和元类的名字一样,但是地址是不一样的。这就解释了为什么XJLPerson类中的resolveInstanceMethod没被调用。

    按照以上分析的逻辑,根元类的父类是NSObject(根类),如果根元类中找不到方法的时候,会在根类中查找,那么我们创建NSObject+XJL的分类,里面实现+resolveInstanceMethod,代码如下:

    #import "NSObject+XJL.h"
    @implementation NSObject (XJL)
    +(BOOL)resolveInstanceMethod:(SEL)sel{
        if(@selector(test) == sel){
            NSLog(@"resolveInstanceMethod--进入%@--",NSStringFromSelector(sel));
        }
        return NO;
    }
    @end
    

    打印结果:

    打印结果
    根代码逻辑是一样的,先会调用resolveClassMethod方法,再调用resolveInstanceMethod方法,而且都会调用2次。

    动态方法决议的具体运用

    resolveClassMethod方法中如果没有动态添加类方法,会调用元类中的resolveInstanceMethod。猜想能不能把resolveInstanceMethod写到一个公用类中,使类方法实例方法都能调用。

    • 实例方法查找流程:对象 --> -->直到根类(NSObject)--> nil
    • 类方法查找流程: --> 元类 -->直到根类(NSObject) --> nil
      最后还是无论是类方法还是实例方法都会走到根类(NSObject)中查找方法,那么我们创建一个NSObject+XJL的分类,来提供动态方法,代码如下:
      动态方法协议的拓展
    • 实例方法类方法都调用了resolveInstanceMethod方法,区别在于开始训着方法的地方实例方法是类中,类方法在元类中。

    动态方法决议优点

    • 可以统一处理方法崩溃的问题,出现方法崩溃可以上报服务器,或者跳转到首页又或许做其他的操作。
    • 如果项目中是不同的模块你可以根据命名不同,进行业务的区别。
    • 这种方式叫切面编程---AOP

    拓展一下AOP和OOP

    • OOP:实际上是对对象的属性和行为的封装,功能相同的抽取出来单独封装,强依赖性高耦合
    • AOP:是处理某个步骤和阶段的,从中进行切面的提取,有重复的操作行为,AOP就可以提取出来,运用动态代理,实现程序功能的统一维护依赖性小耦合度小,单独把AOP提取出来的功能移除也不会对主代码造成影响。AOP更像一个三维的纵轴,平面内的各个类有共同逻辑的通过AOP串联起来,本身平面内的各个类没有任何的关联
      注意:如果需要详细了解AOP的小伙伴可以自行查找一些资料,我这里就引出一下知识点而已哦。

    总结

    学习完动态方法决议后,其实发觉它的主要意义是给多一次机会我们去处理一些崩溃的方法,这样子能够大大提高APP的流畅性和容错性。但是这篇文章说讲的动态方法决议只是引出其使用的场景,这样子操作其实也不是非常的合理的,下一篇文章会在消息的转发里程中得到解决哦,敬请期待!!加油!,

    相关文章

      网友评论

          本文标题:Objective-C 动态方法决议

          本文链接:https://www.haomeiwen.com/subject/cmrcpltx.html