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

本文为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(三)动态决议)