美文网首页
消息转发流程解析

消息转发流程解析

作者: iOSer_jia | 来源:发表于2020-10-09 14:32 被阅读0次

    objc4官方的源码上看,当一个方法经过快速查找和慢速查找后,会经过动态方法决议进行第一次补救,然而苹果仅仅给我们这一次补救机会吗?从源码上我们无法获取更多信息,但我们可以通过其他方式获得方法信息。

    通过log打印方法调用信息

    我们来到lookUpImpOrForward函数,在done的执行流程中,会调用到log_and_fill_cache方法,跟进log_and_fill_cache函数。

    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);
    }
    

    跟进logMessageSend,查看是如何打印消息信息的。

    bool objcMsgLogEnabled = false;
    static int objcMsgLogFD = -1;
    
    bool logMessageSend(bool isClassMethod,
                        const char *objectsClass,
                        const char *implementingClass,
                        SEL selector)
    {
        char    buf[ 1024 ];
    
        // Create/open the log file
        if (objcMsgLogFD == (-1))
        {
            snprintf (buf, sizeof(buf), "/tmp/msgSends-%d", (int) getpid ());
            objcMsgLogFD = secure_open (buf, O_WRONLY | O_CREAT, geteuid());
            if (objcMsgLogFD < 0) {
                // no log file - disable logging
                objcMsgLogEnabled = false;
                objcMsgLogFD = -1;
                return true;
            }
        }
    
        // Make the log entry
        snprintf(buf, sizeof(buf), "%c %s %s %s\n",
                isClassMethod ? '+' : '-',
                objectsClass,
                implementingClass,
                sel_getName(selector));
    
        objcMsgLogLock.lock();
        write (objcMsgLogFD, buf, strlen(buf));
        objcMsgLogLock.unlock();
    
        // Tell caller to not cache the method
        return false;
    }
    

    可以看到,生成的日志回写入到/tmp路径下的文件中去,然而写入文件需要objcMsgLogEnabled为true的条件下才能开启,而objcMsgLogEnabled默认是false,所以我们需要设置这个参数为true。

    而开启这个开关需要调用instrumentObjcMessageSends函数。

    void instrumentObjcMessageSends(BOOL flag)
    {
        bool enable = flag;
    
        // Shortcut NOP
        if (objcMsgLogEnabled == enable)
            return;
    
        // If enabling, flush all method caches so we get some traces
        if (enable)
            _objc_flush_caches(Nil);
    
        // Sync our log file
        if (objcMsgLogFD != -1)
            fsync (objcMsgLogFD);
    
        objcMsgLogEnabled = enable;
    }
    

    现在已经找到开关函数,但是这是objc源码的函数,我们可以使用extern关键字将这个函数暴露出来。

    extern void instrumentObjcMessageSends(BOOL flag);
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // insert code here...
            instrumentObjcMessageSends(YES);
            [[Animal alloc] eat];
            instrumentObjcMessageSends(NO);
            NSLog(@"Hello, World!");
        }
        return 0;
    }
    

    执行之后可以看到/tmp路径下多了一个文件
    [图片上传失败...(image-6a4fae-1602225141126)]

    执行cat /private/tmp/msgSends-94170

    ➜  Deserve git:(master) cat /private/tmp/msgSends-94170 
    + Animal NSObject initialize
    + Animal NSObject alloc
    + Animal NSObject resolveInstanceMethod:
    + Animal NSObject resolveInstanceMethod:
    - Animal NSObject forwardingTargetForSelector:
    - Animal NSObject forwardingTargetForSelector:
    - Animal NSObject methodSignatureForSelector:
    - Animal NSObject methodSignatureForSelector:
    - Animal NSObject class
    + Animal NSObject resolveInstanceMethod:
    + Animal NSObject resolveInstanceMethod:
    - Animal NSObject doesNotRecognizeSelector:
    - Animal NSObject doesNotRecognizeSelector:
    - Animal NSObject class
    - OS_xpc_serializer OS_xpc_object dealloc
    - OS_object NSObject dealloc
    + OS_xpc_payload NSObject class
    - OS_xpc_payload OS_xpc_payload dealloc
    

    可以看到,当一个方法没有找到后,除了会经过一次动态方法解析后,还会在调用forwardingTargetForSelectormethodSignatureForSelector这两个方法,这两个方法便是我们今天讨论的消息转发。

    forwardingTargetForSelector--消息快速转发

    看到苹果官方文档,他对forwardingTargetForSelector的描述是这样的。

    If an object implements (or inherits) this method, and returns a non-nil (and non-self) result, that returned object is used as the new receiver object and the message dispatch resumes to that new object. (Obviously if you return self from this method, the code would just fall into an infinite loop.)
    
    If you implement this method in a non-root class, if your class has nothing to return for the given selector then you should return the result of invoking super’s implementation.
    
    This method gives an object a chance to redirect an unknown message sent to it before the much more expensive forwardInvocation: machinery takes over. This is useful when you simply want to redirect messages to another object and can be an order of magnitude faster than regular forwarding. It is not useful where the goal of the forwarding is to capture the NSInvocation, or manipulate the arguments or return value during the forwarding.
    

    简单来说,我们可以实现这个方法,返回一个有实现目标selector的对象,当一个方法在当前类没有找到实现,并且动态方法决议并没有处理这一selector,此时便会来到快速转发流程,如果我们在类中实现了forwardingTargetForSelector并返回一个非空且不是本类的对象,那么这个方法就会交给返回对象处理。

    @implementation Animal
    
    - (id)forwardingTargetForSelector:(SEL)aSelector {
        if (aSelector == @selector(eat)) {
            return [Cat alloc];
        }
        return  [super forwardingTargetForSelector:aSelector];
    }
    
    @end
    

    控制台打印

    2020-10-08 10:29:40.507728+0800 消息转发[96382:13908868] -[Cat eat]
    

    这样的话,一个原本会导致崩溃的方法便转移给了另一个类处理,这便是快速转发的流程。

    虽然文档说不能转移给self本身,但实际上,我们使用runtime的特性在forwardingTargetForSelector中使用addmethod方法动态地当前类添加方法,此时便不会进入死循环。

    methodSignatureForSelector--慢速消息转发

    This method is used in the implementation of protocols. This method is also used in situations where an NSInvocation object must be created, such as during message forwarding. If your object maintains a delegate or is capable of handling messages that it does not directly implement, you should override this method to return an appropriate method signature.
    

    如果开发者依然没有处理快速转发,那么消息流程就会来到慢速转发,也就是是methodSignatureForSelector方法,这个方法要求返回的是一个函数签名,不仅如此,这个方法还需要搭配另一个方法forwardInvocation使用(如果没有实现依然崩溃),如果我们实现了这两个方法,那么就算我们没有实现当前方法,系统也会把这个事物当作漂流瓶扔出去,而不会导致程序崩溃。我们通过代码体验。

    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
        if (aSelector == @selector(eat)) {
            return [NSMethodSignature signatureWithObjCTypes:"v@:"];
        }
        return [super methodSignatureForSelector:aSelector];
    }
    
    - (void)forwardInvocation:(NSInvocation *)anInvocation {
        NSLog(@"__%s__", __func__);
    }
    

    运行结果:

    2020-10-09 10:06:37.399345+0800 消息转发[23495:14548357] __-[Animal forwardInvocation:]__
    

    当然,我们也可以对当前事物进行处理。

    
    - (void)forwardInvocation:(NSInvocation *)anInvocation {
        NSLog(@"__%s__", __func__);
        anInvocation.selector = @selector(eatFood);
        [anInvocation invoke];
    }
    
    - (void)eatFood {
        NSLog(@"%s", __func__);
    }
    

    或者交给另外的对象接收:

    - (void)forwardInvocation:(NSInvocation *)anInvocation {
        NSLog(@"__%s__", __func__);
        anInvocation.target = [Cat alloc];
        [anInvocation invoke];
    }
    

    我们可以在这里记录程序方法调用失败的错误信息。

    doesNotRecognizeSelector -- 方法没有处理

    doesNotRecognizeSelector的方法定义在objc4源码中。

    // Replaced by CF (throws an NSException)
    - (void)doesNotRecognizeSelector:(SEL)sel {
        _objc_fatal("-[%s %s]: unrecognized selector sent to instance %p", 
                    object_getClassName(self), sel_getName(sel), self);
    }
    

    这便是我们常见的方法调用失败是控制台打印的错误信息,至此,当来到这里时程序崩溃,退出循环。

    总结

    当慢速查找没有找到类的实现,方法会依次经过动态方法决议、消息快速转发、消息慢速转发。

    消息转发流程图.png

    相关文章

      网友评论

          本文标题:消息转发流程解析

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