美文网首页
第十一节—objc_msgSend(三)动态决议

第十一节—objc_msgSend(三)动态决议

作者: L_Ares | 来源:发表于2020-10-28 12:57 被阅读0次

    本文为L_Ares个人写作,以任何形式转载请表明原文出处。

    第九节快速查找流程第十节慢速查找流程之后,方法的查找流程就已经完成了。

    但是在第十节慢速查找流程done之前的最后一步仍然没有找到方法的实现,还没有介绍。那一步就是本节的重点——动态决议,也叫动态方法解析

    动态决议的目的是 :

    给没有找到imp的类一次通过resolveInstanceMethod添加imp的机会。从而不进行崩溃的信息打印,可以实现imp的功能。

    其实上一节我们已经见过它,就是没有走到它的里面去。因为上一节的方法都是实现了的,我们是可以在继承链上找到的,那么这一节,就不让方法实现,去看一下,慢速查找流程中,走到了动态协议是什么情况。

    一、找到动态决议的入口

    就是上一节的步骤,这里就多写一遍,免得再去找了吧。

    还是要用到objc4-781源码。创建两个类 : JDPersonJDStudent。其中,JDStudent继承于JDPerson

    @interface JDPerson : NSObject
    
    - (void)studyWork;
    
    @end
    
    @implementation JDPerson
    
    @end
    
    
    
    @interface JDStudent : JDPerson
    
    - (void)doHomeWork;
    
    + (void)tryYourBest;
    
    + (void)toImpTry;
    
    @end
    
    @implementation JDStudent
    
    
    - (void)doHomeWork
    {
        NSLog(@"doHomeWork");
    }
    
    + (void)toImpTry
    {
        NSLog(@"toImpTry");
    }
    
    @end
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // insert code here...
    
            JDStudent *student = [[JDStudent alloc] init];
            
            [student studyWork];    //挂上断点
            
            NSLog(@"Hello, World!");
            
        }
        return 0;
    }
    
    

    可以看到-(void)studyWork方法我并没有实现。那么我们进入断点并打开汇编。

    图1.png

    然后给objc_msgSend那行挂上断点并走到断点上,然后按住control,点击step into

    图2.png 图3.png

    又来到了_objc_msgSend_uncached。给这行挂上断点,并且走到断点上,继续按住control,点击step into

    图4.png

    依然是lookUpImpOrForward。全局搜索lookUpImpOrForward,找到它在runtime中的实现。

    于是我们又来到了上一节中的位置,这就证明,无论方法是否有实现,lookUpImpOrForward都是在快速查找流程之后,进行慢速查找流程或者动态决议的必经之路。

    拉到done之前,找到如下图的代码片段 :

    图5.png

    这就是我们探索动态决议的入口。

    二、动态决议的实现

    从图5我们可以看到,动态决议的这一步主要就是return里面的那个函数,点进去。

    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 {
            //如果是元类,则类方法决议解析
            resolveClassMethod(inst, sel, cls);
            
            //如果还是没有找到
            if (!lookUpImpOrNil(inst, sel, cls)) {
                //调用元类的实例方法的动态方法解析
                resolveInstanceMethod(inst, sel, cls);
            }
        }
    
        //调用的动态解析可能已经填充了缓存,所以再尝试一次lookUpImpOrForward慢速查找
        return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
    }
    

    思路 :

    先判断锁的情况和类的合法性,类是否实现。
    (1). 检查传入的类不是元类。使用实例方法的动态决议。
    (2). 检查传入的类是元类。使用类方法的动态决议。如果在元类的类方法的动态决议还是没有找到。因为类方法在元类中属于实例方法,所以再找元类的实例方法的动态决议。
    (3). 在动态决议的过程中可能imp已经填充到了缓存,所以再执行一次慢速查找流程。

    那么这里就要分情况看实例方法的动态决议类方法的动态决议

    1. 实例方法的动态决议

    static void resolveInstanceMethod(id inst, SEL sel, Class cls)
    {
        //还是先检查锁和类是否实现
        runtimeLock.assertUnlocked();
        ASSERT(cls->isRealized());
        
        //定义一个SEL变量,保存`resolveInstanceMethod`方法
        SEL resolve_sel = @selector(resolveInstanceMethod:);
    
        //到慢速查找流程中找当前类的父类是否实现了resolveInstanceMethod
        //如果没有实现,直接就return了,所以这个一定会实现的,不然就不用动态决议了,直接就跳出函数了
        //resolveInstanceMethod在全局搜索可以在NSObject中找到,也就是说NSObject已经帮我们实现了
        //沿着继承链查找,最后到根类的时候一定会找到这个方法的实现的,而且返回的是一个BOOL值NO,可以自行查找一下看看
        if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
            // Resolver not implemented.
            return;
        }
    
        //定义了msg就是一个objc_msgSend发送消息
        BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
        
        //像当前的类cls发送一条消息,resolveInstanceMethod函数,并且参数是你的`sel`
        //这句代码会直接查询快速流程,因为上面说过了,NSObject实现了resolveInstanceMethod
        //在上面那句代码调用lookUpImpOrNil的时候,还记得done里面做了什么吧,直接把对应的sel-imp存入到了当前的cls里面吧
        //所以这里的msg也就是objc_msgSend在快速查找的时候查找缓存就会找到当前的cls里面存在的resolveInstanceMethod是否实现
        bool resolved = msg(cls, resolve_sel, sel);
    
        //再次查找sel的imp有没有被你在cls的继承链上实现,如果有,肯定会返回imp,如果没有就只能返回nil了
        IMP imp = lookUpImpOrNil(inst, sel, cls);
    
        //如果resolveInstanceMethod在cls里面实现了
        if (resolved  &&  PrintResolving) {
            
            //如果sel也找到了imp
            if (imp) {
                //会打印这个imp是实例方法还是类方法和sel名字等信息
                _objc_inform("RESOLVE: method %c[%s %s] "
                             "dynamically resolved to %p", 
                             cls->isMetaClass() ? '+' : '-', 
                             cls->nameForLogging(), sel_getName(sel), imp);
            }
            else {
                // 如果没有找到imp,证明cls在+resolveInstanceMethod方法里面什么都没做。
                _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));
            }
        }
    }
    
    static inline IMP
    lookUpImpOrNil(id obj, SEL sel, Class cls, int behavior = 0)
    {
        return lookUpImpOrForward(obj, sel, cls, behavior | LOOKUP_CACHE | LOOKUP_NIL);
    }
    

    注释一定要看,不然看不懂这里的思路

    思路 :

    • 先知道+ (BOOL)resolveInstanceMethod:(SEL)sel这个方法是在NSObject中实现的,默认返回的是NO

    • 因为函数的开始执行了一次lookUpImpOrNil,其实就是调用了lookUpImpOrForward慢速查找流程,查找cls继承链上的resolveInstanceMethod方法实现。因为当前类cls一定会因为根类是NSObject,所以一定可以在NSObject找到resolveInstanceMethod的实现。从而会走到lookUpImpOrForwarddone方法里面,将resolveInstanceMethod放入cls的方法缓存。

    • 然后利用objc_msgSend发送消息给cls去寻找sel = resolveInstanceMethodimp实现。并获取到一个BOOL值判断cls有没有实现resolveInstanceMethod
      (1). 所以你可以在cls里面实现resolveInstanceMethod,添加selimp。下面会举例说明。
      (2). 如果你不在cls里面添加selimp,那么clssel还是找不到imp

    • 再次利用lookUpImpOrNil也就是lookUpImpOrForward慢速查找clssel是否有对应的imp
      (1). 如果有,那么就会执行imp
      (2). 如果没有,证明resolveInstanceMethod没有被你自定义实现,还是返回的默认的NOsel也依然找不到对应的imp

    举例 :

    还是文章最开始的那段代码,我在@implementation JDStudent里面添加+ (BOOL)resolveInstanceMethod:(SEL)sel,并且给它一个实现。

    + (BOOL)resolveInstanceMethod:(SEL)sel {
        
        NSLog(@"进入到了动态方法解析");
        
        if (sel == @selector(studyWork)) {
    
            NSLog(@"手动添加imp实现");
    
            IMP swIMP = class_getMethodImplementation(self, @selector(doHomeWork));
    
            Method swMethod = class_getInstanceMethod(self, @selector(doHomeWork));
    
            const char* swType = method_getTypeEncoding(swMethod);
    
            class_addMethod(self, sel, swIMP, swType);
    
        }
        
        return [super resolveInstanceMethod:sel];
    }
    

    你会看到控制台的结果是 :

    图6.png

    这就证明,虽然JDPerson没有实现studyWork,它的子类JDStudent在快速流程和慢速流程中都找不到sel == studyWorkimp实现,但是经过动态决议,手动的将studyWorkimp添加了JDStudentdoHomeWork的实现。

    如果不添加if里面的内容则会出现如下图所示的错误 :

    图7.png

    这个错误就是在慢速查找流程中imp = forward_imp = (IMP)_objc_msgForward_impcache拿到的错误信息。

    问题 :

    但是这个里面还有一个问题,如果不手动添加selimp的情况下,我明明就只执行了一次动态决议的方法resolveInstanceMethod,为什么打印出来是执行了两次?

    这只能证明系统在其他的地方还是做过了处理。先不看这个问题,先继续动态决议的主线流程,这个问题在下一节再说。

    2. 类方法的动态决议

    在最开始的代码的main.m中,我们让类JDStudent执行类方法+ (void)tryYourBest;

    类方法tryYourBest依然是没有实现的。

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // insert code here...
         
            JDStudent *student = [[JDStudent alloc] init];
    
            [student studyWork];
            
            [JDStudent tryYourBest];
            
            NSLog(@"Hello, World!");
            
        }
        return 0;
    }
    

    查看结果。

    图8.png

    不是给JDStudent添加了动态决议的实现了吗?为什么没走进去呢?那就证明现在的resolveInstanceMethod对类方法没生效。

    于是就回到了这里。

    图9.png

    于是我们来看resolveClassMethod方法。

    static void resolveClassMethod(id inst, SEL sel, Class cls)
    {
        //检查锁、类的实现、cls是不是元类
        runtimeLock.assertUnlocked();
        ASSERT(cls->isRealized());
        ASSERT(cls->isMetaClass());
    
        //第一步和实例方法的动态决议思路是一样的。请看实例方法的动态决议的注释
        //不一样的是,这次检查的是resolveClassMethod。
        if (!lookUpImpOrNil(inst, @selector(resolveClassMethod:), cls)) {
            // Resolver not implemented.
            return;
        }
    
        //这个就是获取cls的元类,会判断cls是不是就是元类,如果是直接返回cls,如果不是,则找cls的元类并且返回给nonmeta
        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 = lookUpImpOrNil(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 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));
            }
        }
    }
    

    思路 :

    其实大体的思路和实例方法的动态决议是一样的。

    所以是不是我们可以在JDPerson中添加resolveClassMethod的实现,来解决imp找不到的问题。

    于是在JDPerson@implementation中添加+ (BOOL)resolveClassMethod:(SEL)sel,并实现

    + (BOOL)resolveClassMethod:(SEL)sel {
        
        NSLog(@"进入到了类的动态解析方法");
        
        if (sel == @selector(tryYourBest)) {
            
            NSLog(@"手动添加类方法的imp实现");
            
            IMP trIMP = class_getMethodImplementation(objc_getMetaClass("JDStudent"), @selector(toImpTry));
            
            Method trMethod = class_getInstanceMethod(objc_getMetaClass("JDStudent"), @selector(toImpTry));
            
            const char* trType = method_getTypeEncoding(trMethod);
            
            return class_addMethod(objc_getMetaClass("JDStudent"), sel, trIMP, trType);
            
        }
        
        
        return [super resolveClassMethod:sel];
    }
    
    

    结果 :

    图10.png

    的确解决了问题。那么如果还是只给实现resolveClassMethod但是不给tryYourBest添加imp呢?

    图11.png

    一样会出现和实例方法的动态决议相同的情况。 这个还是放在后面一节和实例方法的问题一起说。

    3. 关于一些操作的优缺点

    这个思路网上很多人也说过了,因为类方法的动态决议resolveClassMethod如果没有找到实现的话,还是会检查元类的resolveInstanceMethod方法,所以其实在元类里面实现resolveInstanceMethod的话,是不是就不用写两个了?

    但是元类不是我们可以操控的呀,于是就找到了更下一层的NSObject,然后就开始对NSObject加上分类,在NSObject的分类里面实现resolveInstanceMethod。反正底层在找sel-imp的时候不是根据什么实例方法和类方法来找的,都是根据name来找的吧,那只要判断name就可以了嘛。

    但是这样是有缺点的,如果没有实现的方法很多,难道全部都跑到NSObject的分类里面去处理吗?耦合性太高了吧,而且还有一堆的if判断,读写起来都困难。

    所以也提供了一种在方法名前面统一前缀的方式,比如某一个类的方法名前面全都是jd_,这个是不是也可以?利用NSStringFromSelector拿到方法名的字符串,然后判断字符串是不是以jd_开头的,如果是的话,做统一的处理。

    但是呢,如果碰到其他人直接在上层的类里面就做了resolveInstanceMethod的实现,那是不是又没用了,所以优缺点都是有的,怎么去使用,或者说大家怎么配合,也是能尽量避坑的关键。

    相关文章

      网友评论

          本文标题:第十一节—objc_msgSend(三)动态决议

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