美文网首页
iOS看源码:消息转发

iOS看源码:消息转发

作者: FireStroy | 来源:发表于2020-07-08 20:35 被阅读0次

    消息的发送

    前篇
    iOS看源码:消息发送01
    iOS看源码:方法缓存
    iOS看源码:方法慢速查找
    消息发送的本质是objc_msgsend(),会先从消息接受者的缓存中查找,缓存中找不到则按照isa的指向依次按照由本类向父类直到根类NSObject的方法列表中查找。

    消息动态转发

    lookUpImpOrForward()各种流程都没找到方法实现 那么就会返回一个系统默认的(IMP)_objc_msgForward_impcache方法实现。
    这就是让你看到*** unrecognized selector sent to instance ***这条崩溃信息的函数。

    但是 ,在程序崩溃之前,你还是有为这条没有找到IMP的方法提供一个IMP的机会。

    当上面的消息查找流程没有找到IMP时,会有一次时机执行方法的转发机制。

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

    动态转发过程

    通过对标志位behavior的判断和操作,对没有进行过动态转发的消息进行一次转发机制resolveInstanceMethod ()

    static NEVER_INLINE IMP
    resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
    {
        runtimeLock.assertLocked();
        ASSERT(cls->isRealized());
        runtimeLock.unlock();
        if (! cls->isMetaClass()) {
            resolveInstanceMethod(inst, sel, cls);
        } 
        else {
            resolveClassMethod(inst, sel, cls);
            if (!lookUpImpOrNil(inst, sel, cls)) {
                resolveInstanceMethod(inst, sel, cls);
            }
        }
        return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
    }
    

    当走到这一步的时候,runtime会先查询对象是否实现了resolveInstanceMethod()这个方法,如果实现了这个方法就直接调用这个方法。
    然后会再执行一次方法的查找流程lookUpImpOrNil()
    这里为什么要再执行一次查找流程呢?
    因为resolveInstanceMethod()这个方法就是提供一个让你提供一个被查找方法的IMP的机会。

    static void resolveInstanceMethod(id inst, SEL sel, Class cls)
    {
        runtimeLock.assertUnlocked();
        ASSERT(cls->isRealized());
        SEL resolve_sel = @selector(resolveInstanceMethod:);
    
        if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
            return;
        }
    
        BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
        bool resolved = msg(cls, resolve_sel, sel);
    
        IMP imp = lookUpImpOrNil(inst, sel, cls);
        if (resolved  &&  PrintResolving) {
          //...
        }
    }
    

    动态决议第一阶段

    先调用resolveInstanceMethod 为对象提供一次解决方案,如果对象实现了个方法并且提供了所查找方法的IMP则再次进行lookUpImpOrNil()方法查找流程继续走消息发送正常流程。

    用代码演示一下:

    @interface MyPerson : NSObject
    - (void)say;
    @end
    
    @implementation MyPerson
    @end
    
    int main(int argc, const char * argv[]) {
        MyPerson *person = [[MyPerson alloc]init];
        [person say];
        return 0;
    }
    

    person发送了一条没有实现过的say()方法程序直接崩溃

    接下来让Person类实现了+ resolveInstanceMethod();方法并且添加了一个- cry()的方法实现,并且吧这个实现作为@selector(say)的方法实现。

    - (void)cry{NSLog(@"say-->cry");}
    
    + (BOOL)resolveInstanceMethod:(SEL)sel{
        if (sel == @selector(say)) {
            IMP sayCry = class_getMethodImplementation(self, @selector(cry));
            Method method = class_getInstanceMethod(self, @selector(cry));
            const char* type = method_getTypeEncoding(method);
            return class_addMethod(self, sel, sayCry, type);
        }
        return [super resolveClassMethod:sel];
    }
    

    结果就是程序没有崩溃,原来的-say方法最终调用的是-cry方法的实现。

    消息转发成功

    这一阶段的转发,需要注意类方法和实例方法 不过最后的本质是一样的,只是区别于类和元类,最后的根元类也最终指向了NSObject类。
    注意,如果你把这个转发过程放在了NSObject的分类中去实现而不不加处理,那么你可能就覆盖了很多系统级别的消息转发。

    + (BOOL)resolveClassMethod:(SEL)sel;
    + (BOOL)resolveInstanceMethod:(SEL)sel;
    

    第二阶段

    如果第一个阶段,我们并没有去实现那怎么办?
    再开始这一阶段讲解之前,先讲一个小技巧。
    前面在讲解方法缓存的时候会遇到这一步代码

    static void
    log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
    {
    #if SUPPORT_MESSAGE_LOGGING
        if (slowpath(objcMsgLogEnabled && implementer)) {
            bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                          cls->nameForLogging(),
                                          implementer->nameForLogging(), 
                                          sel);
            if (!cacheIt) return;
        }
    #endif
        cache_fill(cls, sel, imp, receiver);
    }
    

    这里SUPPORT_MESSAGE_LOGGING提示我们logMessageSend()是一个方法日志,它可以吧方法的调用记录下来。

    开启的方法演示一下:

    extern void instrumentObjcMessageSends(BOOL flag);
    int main(int argc, const char * argv[]) {
        MyPerson *person = [[MyPerson alloc]init];
        instrumentObjcMessageSends(true);
        [person say];
        instrumentObjcMessageSends(false);
        return 0;
    }
    

    这样我们就可以查看方法崩溃前,都调用过什么。
    回到第一步的崩溃案例,在崩溃方法前开启日志打印,这样会在Mac的tmp目录下记录调用过程。

    log日志

    我们看到 崩溃前,调用了几个我们并不常见到的方法

    1. + resolveInstanceMethod
    2. - forwardingTargetForSelector
    3. - methodSignatureForSelector
    4. - doesNotRecognizeSelector
      第一个我们知道了,就是动态转发的第一个步骤。
      那么接下来的2、3、4是做什么的呢?
      NSObject的头文件供了这些方法。
    - (void)doesNotRecognizeSelector:(SEL)aSelector;
    - (id)forwardingTargetForSelector:(SEL)aSelector OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
    - (void)forwardInvocation:(NSInvocation *)anInvocation OBJC_SWIFT_UNAVAILABLE("");
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector OBJC_SWIFT_UNAVAILABLE("");
    

    具体怎么用去看看官方文档提供的信息。


    动态决议二阶段-快速转发

    - forwardingTargetForSelector:这个方法叫我们找一个能够解决这个SEL实现的对象。自己不能解决,或许可以交给别人去解决。
    自己的类没有实现-say这个放么,把问题甩给MyStudent的实例对象去解决。相应的MyStudent这个类如果实现了这个甩过来的方法,就会执行调用。
    代码演示:

    @interface MyStudent : NSObject
    - (void)say;
    @end
    
    @implementation MyStudent
    - (void)say{NSLog(@"student - say");}
    @end
    
    @implementation MyPerson
    - (id)forwardingTargetForSelector:(SEL)aSelector{
        if (aSelector == @selector(say)) {
            return [[MyStudent alloc] init];
        }
        return [super forwardingTargetForSelector:aSelector];
    }
    @end
    
    person把消息给了student并调用

    第三阶段

    第二阶段讲了,自己没有实现的消息可以转发给其他实现了这个消息的对象。那么万一没有人实现了这个消息怎么办?
    如果第二阶段失败,那么就进入第三阶段 消息的慢速转发

    动态决议第三阶段-消息的慢速转发

    - methodSignatureForSelector:方法签名的处理
    消息到了这一步会获取到一个NSInvocation对象,里面包装了方法名和一些参数,这时候你可以选择让合适的对象来处理它invoke,当然也可以不调用。最少可以把程序崩溃在这里处理了。

    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
        if (aSelector == @selector(say)) {
            return [NSMethodSignature signatureWithObjCTypes:"v@:"];
        }
        return [super methodSignatureForSelector:aSelector];
    }
    
    - (void)forwardInvocation:(NSInvocation *)anInvocation{
        SEL aSelector = [anInvocation selector];
        id obj = [MyStudent alloc];
           if ([[MyStudent alloc] respondsToSelector:aSelector])
               [anInvocation invokeWithTarget:[MyStudent alloc]];
           else
               [super forwardInvocation:anInvocation];
    }
    

    最后的崩溃

    如果你没有实现第三步那么程序就真的要走崩溃流程了
    - doesNotRecognizeSelector:然后闪退。

    相关文章

      网友评论

          本文标题:iOS看源码:消息转发

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