美文网首页
(2021 objc4-818源码分析)方法查找流程-动态决议&

(2021 objc4-818源码分析)方法查找流程-动态决议&

作者: 丸疯 | 来源:发表于2021-01-15 17:09 被阅读0次

    方法查找流程

    【第一步】 方法查找流程-快速查找流程
    【第二步】 方法查找流程-慢速查找
    【第三步】 动态决议
    【第四步】 消息转发

    动态决议

    方法查找流程-慢速查找中,探索到如果通过慢速查找都没有找到,则会进行动态方法决议

        // No implementation found. Try method resolver once.
    
        if (slowpath(behavior & LOOKUP_RESOLVER)) {
            behavior ^= LOOKUP_RESOLVER;
            return resolveMethod_locked(inst, sel, cls, behavior);
        }
    

    resolveMethod_locked源码

    static NEVER_INLINE IMP
    resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
    {
        runtimeLock.assertLocked();
        ASSERT(cls->isRealized());
    
        runtimeLock.unlock();
    
        if (! cls->isMetaClass()) {
            // try [cls resolveInstanceMethod:sel]
            resolveInstanceMethod(inst, sel, cls);
        } 
        else {
            // try [nonMetaClass resolveClassMethod:sel]
            // and [cls resolveInstanceMethod:sel]
            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);
    }
    
    动态方法决议图解.png
    • 如果没有找到就会调用resolveMethod_locked(inst, sel, cls, behavior)
    • 判断cls是否是元类
      • cls是元类
        • 执行resolveClassMethod(inst, sel, cls),使用类方法的动态方法决议
        • lookUpImpOrNilTryCache(inst, sel, cls),类方法解析是否找到或者为空
          • 如果没有找到或者为空,则执行resolveInstanceMethod(inst, sel, cls)
      • cls不是元类
        • 执行resolveInstanceMethod(inst, sel, cls),使用对象方法的动态方法决议

    解决方法没有实现的思路一

    通过上面的分析,我们知道如果没有找到方法的实现,会进行动态方法决议,那我们可以尝试针对类方法没有实现我们可以去实现resolveClassMethod,对象方法没有实现我们可以去实现resolveInstanceMethod

    我们声明一个类,分别定义一个类方法和对象方法,不去实现它

    + (void)sayHello;
    - (void)sayNB;
    
    resolveClassMethod的实现

    我们直接调用没有实现的类方法sayHello,运行发生崩溃,控制台打印结果如下:

    +[Person sayHello]: unrecognized selector sent to class 0x100008208
    

    解决方法:

    + (void)resolveClassMethodNotFound{
        NSLog(@"%s", __func__);
    }
    
    + (BOOL)resolveClassMethod:(SEL)sel{
        if (sel == @selector(sayHello)) {
            NSLog(@"%@ 来了", NSStringFromSelector(sel));
                
            IMP imp = class_getMethodImplementation(objc_getMetaClass("Person"), @selector(resolveClassMethodNotFound));
            Method solveClassMethod  = class_getInstanceMethod(objc_getMetaClass("Person"), @selector(resolveClassMethodNotFound));
            const char *type = method_getTypeEncoding(solveClassMethod);
            return class_addMethod(objc_getMetaClass("Person"), sel, imp, type);
        }
        return [super resolveClassMethod:sel];
    }
    

    再次运行的结果

    sayHello 来了
    +[Person resolveClassMethodNotFound]
    

    分析
    通过上面的代码,确实在慢速查找没有找打方法的实现的时候,类方法会来到resolveClassMethod,此时,我们实现resolveClassMethod,就能防止崩溃,这里的做法是把当前的方法,动态的添加一个类方法,并将sayHello的实现转移给新添加的方法

    resolveInstanceMethod的实现

    我们直接调用sayNB对象方法,会报方法找不到的报错如下:

    -[Person sayNB]: unrecognized selector sent to instance 0x100442da0
    

    会提示实例方法,找不到。unrecognized selector sent to instance

    解决方案:

    - (void)resolveinstanceMethodNotFound{
        NSLog(@"%s", __func__);
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)sel{
        if (sel == @selector(sayNB)) {
            NSLog(@"%@ 来了", NSStringFromSelector(sel));
            IMP imp = class_getMethodImplementation(self, @selector(resolveinstanceMethodNotFound));
            Method solveMethod  = class_getInstanceMethod(self, @selector(resolveinstanceMethodNotFound));
            const char *type = method_getTypeEncoding(solveMethod);
            return class_addMethod(self, sel, imp, type);
        }
        return [super resolveInstanceMethod:sel];
    }
    

    这里没有找到sayNB的方法实现,来到resolveInstanceMethod,只需动态的添加一个方法,即可视做完成了sayNB的实现。这里的做法是获取resolveinstanceMethodNotFoundimp,和签名。绑定到sayNBsel,我们知道方法的存储是通过sel & _mask来获取得到存储下标,这样我们在调用sayNB的时候,默认调用的就是resolveinstanceMethodNotFound的实现。

    修改之后的结果

     sayNB 来了
     -[Person resolveinstanceMethodNotFound]
    

    resolveMethod_locked的实现中,我们看到如下代码

        if (! cls->isMetaClass()) {
            // try [cls resolveInstanceMethod:sel]
            resolveInstanceMethod(inst, sel, cls);
        } 
        else {
            // try [nonMetaClass resolveClassMethod:sel]
            // and [cls resolveInstanceMethod:sel]
            resolveClassMethod(inst, sel, cls);
            if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
                resolveInstanceMethod(inst, sel, cls);
            }
        }
    

    对于类方法,再执行了resolveClassMethod之后,会尝试lookUpImpOrNilTryCache,从缓存中去找,如果没有找到,也会执行resolveInstanceMethod实例方法的动态决议,是不是意味着我们不用实现resolveClassMethod只实现一个resolveInstanceMethod就行了。因为元类没有实体类,沿着继承链我们只能找到NSObject,所以我们给NSObject添加一分类,并实现resolveInstanceMethod,来验证一下:

    处理方案

    • 在NSObject+SolveInstance.h中添加如下代码,
    • 因为NSObject是根类,super为nil,所有我们这里直接返回NO。
    • 我们只处理明确知道的sayNBsayHello两个没有实现的方法,避免产生不必要的崩溃
    • 不管是类方法,还是实例方法,找到NSObject都会变成实例方法,所以这里返回转移的方法实现定义为实例方法就行了
    - (void)resolveinstanceMethodNotFound{
        NSLog(@"%s", __func__);
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)sel{
        if (sel == @selector(sayHello) || sel == @selector(sayNB)) {
            NSLog(@"%@ 来了", NSStringFromSelector(sel));
            IMP imp = class_getMethodImplementation(self, @selector(resolveinstanceMethodNotFound));
            Method solveMethod  = class_getInstanceMethod(self, @selector(resolveinstanceMethodNotFound));
            const char *type = method_getTypeEncoding(solveMethod);
            return class_addMethod(self, sel, imp, type);
        }
        return NO;
    }
    

    跟我们预想的一样,打印结果如下

     sayHello 来了
     -[NSObject(SolveInstance) resolveinstanceMethod\M-LotFound]
     sayNB 来了
     -[NSObject(SolveInstance) resolveinstanceMethod\M-LotFound]
    

    充分印证了:


    isa与继承链.png

    消息转发

    打开消息发送的日志

    我们跟log_and_fill_cache的源码的时候发现,消息的日志存储在/tmp/msgSends

    消息发送日志存储.png
    但是我们在这个目录下,并没有找到消息发送的日志,缺少了条件objcMsgLogFD == (-1),打开这个条件的方式如下:
    extern void instrumentObjcMessageSends(BOOL flag);
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // insert code here...
            Person *person = [Person alloc];
            instrumentObjcMessageSends(YES);
            [person sayNB];
            instrumentObjcMessageSends(NO);
            NSLog(@"Hello, World!");
        }
        return 0;
    }
    

    这时我们就能拿到sayNB调用之后的消息日志了,不做任何崩溃处理,直接运行,在/tmp/msgSends目录下就获取到了消息日志

    sayNB的消息调用日志.png
    通过日志我们看到,再执行动态方法决议之后,还执行了forwardingTargetForSelectormethodSignatureForSelector
    通过文档,我们还知道methodSignatureForSelector需要和forwardInvocation搭配起来使用。
    接下来开始验证
    消息转发-快速转发

    forwardingTargetForSelector
    在Person的.m文件中实现如下代码

    实例方法-消息转发(快速转发)
    /// 快速转发
    - (id)forwardingTargetForSelector:(SEL)aSelector{
        NSLog(@"%s- %@", __func__, NSStringFromSelector(aSelector));
        return [Student alloc];
    }
    
    // Student.m中的实现
    - (void)sayNB {
        NSLog(@"%s", __func__);
    }
    

    打印结果如下

    2021-01-18 15:38:14.107644+0800 消息转发流程[16585:199229] -[Person forwardingTargetForSelector:]- sayNB
    2021-01-18 15:38:14.109524+0800 消息转发流程[16585:199229] -[Student sayNB]
    

    实例方法可以转交给类方法么?代码如下

    /// 快速转发
    - (id)forwardingTargetForSelector:(SEL)aSelector{
        NSLog(@"%s- %@", __func__, NSStringFromSelector(aSelector));
        return [Student class];
    }
    
    // Student.m中的实现
    + (void)sayNB {
        NSLog(@"%s", __func__);
    }
    

    打印如下:

    2021-01-18 15:39:12.076271+0800 消息转发流程[16606:199968] -[Person forwardingTargetForSelector:]- sayNB
    2021-01-18 15:39:12.077819+0800 消息转发流程[16606:199968] +[Student sayNB]
    

    小结
    对于实例方法,快速转发需要重写实例方法forwardingTargetForSelector

    • 如果转发的是一个,那么转发的类必须实现该消息的类方法实现

    • 如果是个对象,则需要实现该方法的实例方法实现
      如果以上不对应,或者没有实现,则会报错

    • 类方法-消息转发(快速转发)

    /// 快速转发
    + (id)forwardingTargetForSelector:(SEL)aSelector{
        NSLog(@"%s- %@", __func__, NSStringFromSelector(aSelector));
        return [Teacher class];
    }
    
    // Teacher.m的方法实现
    + (void)sayHello{
        NSLog(@"%s", __func__);
    }
    

    运行结果

    2021-01-18 15:24:28.039137+0800 消息转发流程[16383:193004] +[Person forwardingTargetForSelector:]- sayHello
    2021-01-18 15:24:28.040839+0800 消息转发流程[16383:193004] +[Teacher sayHello]
    

    还可以有另外一种方式,代码如下

    + (id)forwardingTargetForSelector:(SEL)aSelector{
        NSLog(@"%s- %@", __func__, NSStringFromSelector(aSelector));
        return [Teacher alloc];
    }
    // Teacher.m的方法实现
    - (void)sayHello{
        NSLog(@"%s", __func__);
    }
    

    运行结果

    2021-01-18 15:31:07.617114+0800 消息转发流程[16474:195831] +[Person forwardingTargetForSelector:]- sayHello
    2021-01-18 15:31:07.619068+0800 消息转发流程[16474:195831] -[Teacher sayHello]
    

    小结
    对于类方法,快速转发需要重写类方法forwardingTargetForSelector

    • 如果转发的是一个,那么转发的类必须实现该消息的类方法实现
    • 如果是个对象,则需要实现该方法的实例方法实现
      如果以上不对应,或者没有实现,则会报错

    快速转发总结

    • 类方法和实例方法有自己对应的forwardingTargetForSelector实现,需要重写对应的forwardingTargetForSelector方法
    • 当前类或对象处理不了,可以交给其它类或者对象来处理。
    • 如果转交的是,则需要实现同名类方法
    • 如果转交的是对象,则需要实现同名的实例方法
    • 如果其它类或对象没有处理或上面两个规则不对应,就会报错,所转发的类没有找打改方法的实现
    消息转发-慢速转发

    methodSignatureForSelector
    forwardInvocation
    在Person.m 文件中写下如下代码

    /// 慢速转发
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
        NSLog(@"%s- %@", __func__, NSStringFromSelector(aSelector));
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    
    - (void)forwardInvocation:(NSInvocation *)anInvocation{
        NSLog(@"%s- %@", __func__, anInvocation);
    }
    

    运行,发现不报错,运行结果如下

    2021-01-18 16:43:19.517884+0800 消息转发流程[17265:220490] -[Person methodSignatureForSelector:]- sayNB
    2021-01-18 16:43:19.520205+0800 消息转发流程[17265:220490] -[Person forwardInvocation:]- <NSInvocation: 0x1004078a0>
    

    forwardInvocation中加上如下代码

    - (void)forwardInvocation:(NSInvocation *)anInvocation{
        NSLog(@"%s- %@", __func__, anInvocation);
        [anInvocation invokeWithTarget:[Student alloc]];
    }
    

    打印结果

    2021-01-18 16:46:21.053838+0800 消息转发流程[17325:222827] -[Person methodSignatureForSelector:]- sayNB
    2021-01-18 16:46:21.059838+0800 消息转发流程[17325:222827] -[Person forwardInvocation:]- <NSInvocation: 0x100606dd0>
    2021-01-18 16:46:21.061812+0800 消息转发流程[17325:222827] -[Student sayNB]
    

    同样anInvocationtarget可以是一个类,当需要实现同名的类方法
    总结

    • methodSignatureForSelector需要搭配forwardInvocation使用,forwardInvocation系统会自动调用。
    • forwardInvocation可以不处理anInvocation事物,不会引发崩溃
    • anInvocationtarget可以是,也可以是实例对象,若指定了或者对象,必须实现对应的同名类方法或实例方法

    我们将,动态方法决议,消息转发-快速转发,消息转发-慢速转发都放开,只在消息转发-慢速转发中处理,代码如下

    /// 动态方法决议
    + (BOOL)resolveInstanceMethod:(SEL)sel{
        NSLog(@"%s - %@",__func__,NSStringFromSelector(sel));
        return [super resolveInstanceMethod:sel];
    }
    
    /// 快速转发
    - (id)forwardingTargetForSelector:(SEL)aSelector{
        NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
        return [super forwardingTargetForSelector:aSelector];
    }
    
    /// 慢速转发
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
        NSLog(@"%s- %@", __func__, NSStringFromSelector(aSelector));
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    - (void)forwardInvocation:(NSInvocation *)anInvocation{
        NSLog(@"%s- %@", __func__, anInvocation);
        [anInvocation invokeWithTarget:[Student alloc]];
    }
    

    得出如下结果:

    2021-01-18 17:27:22.544241+0800 消息转发流程[17843:242496] +[Person resolveInstanceMethod:] - sayNB
    2021-01-18 17:27:22.546082+0800 消息转发流程[17843:242496] -[Person forwardingTargetForSelector:] - sayNB
    2021-01-18 17:27:22.546903+0800 消息转发流程[17843:242496] -[Person methodSignatureForSelector:]- sayNB
    2021-01-18 17:27:22.547638+0800 消息转发流程[17843:242496] +[Person resolveInstanceMethod:] - _forwardStackInvocation:
    2021-01-18 17:27:22.549293+0800 消息转发流程[17843:242496] -[Person forwardInvocation:]- <NSInvocation: 0x100604450>
    2021-01-18 17:27:22.549812+0800 消息转发流程[17843:242496] -[Student sayNB]
    

    我们再去消息日志里面查看,得到的调用顺序同样是如上面所示。


    消息转发图解.png
    总结
    • 动态方法决议,在整个环节会被调用两次
      【第一次】在慢速查找没有查找的进入动态方法决议的时候是拯救查找的方法,
      【第二次】在慢速转发过程中调用是拯救_forwardStackInvocation:,可以直接在resolveInstanceMethod处理
    • 需要注意转发的是还是对象,做出对应的实现。

    相关文章

      网友评论

          本文标题:(2021 objc4-818源码分析)方法查找流程-动态决议&

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