消息转发机制

2020-09-25  本文已影响0人  深圳_你要的昵称

前言

书接上回方法慢速查找过程分析,我们知道,在lookUpImpOrForward中如果找到了方法实现imp,那么就会走到log_and_fill_cache去缓存方法,未找到方法imp,会进入resolveMethod_locked动态方法决议,如果动态方法决议中仍未处理的话,后面系统会将消息进行转发,下面我们来看看消息转发的流程。

1.消息转发入口

首先跟之前慢速查找流程一样,先看看消息转发的入口在哪里?我们先看看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);
}

注意,有这个宏SUPPORT_MESSAGE_LOGGING -->满足条件的话会打印日志logMessageSend,源码如下:

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

上面源码中,通过对objcMsgLogFD值的判断,来处理日志文件目录的创建secure_open,以及后面日志的写入write (objcMsgLogFD, buf, strlen(buf));,看来这个objcMsgLogFD很关键,我们再来全局搜下给它赋值的地方,如下图:

objcMsgLogFD调用搜索.png

发现红框处是赋值的地方,在方法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;
}

根据源码大致可以看出,这个方法是用来控制logMessageSend日志记录的,那么我们可以借助该方法,查查在动态方法决议后消息仍未处理的情况下,后面系统将调用哪些方法?

那么如何使用呢?因为这个方法是在.mm实现文件里,无法被外边调用,此时需要使用关键字extern

extern void instrumentObjcMessageSends(BOOL flag);

再查看logMessageSend里打印日志的路径在哪里?

 snprintf (buf, sizeof(buf), "/tmp/msgSends-%d", (int) getpid ());

根据上面代码,在/tmp/路径下,生成的msgSends-名称开头的文件。

示例验证--打印消息调用日志记录

首先创建新工程千万别用源码的工程,创建类LGPerson

@interface LGPerson : NSObject

- (void)sayHello;

@end
//----------------分割线------------------
@implementation LGPerson
@end

只声明sayHello方法,但不实现该方法。
在main入口中,调用该方法如下:

extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        LGPerson *s = [LGPerson alloc];
        instrumentObjcMessageSends(YES);
        [s sayHello];
        instrumentObjcMessageSends(NO);
    }
    return 0;
}

运行代码,打开Finder,前往/tmp/路径查看,多了个msgSends文件

msgSends.png
打开文件,在动态方法决议resolveInstanceMethod :后执行的是forwardingTargetForSelector:methodSignatureForSelector:,找到了,这就是消息转发的入口!

2.消息转发具体流程

上面我们找到了消息转发的入口方法,先后调用了forwardingTargetForSelector:methodSignatureForSelector:,分别看看这两个方法的具体含义。哪里看?-->当然是苹果爸爸的官方文档搜索

2.1 forwardingTargetForSelector

首先查看官方文档说明

forwardingTargetForSelector.png
方法描述大致是:会优先调用这个方法,返回一个对象,去处理这个未识别的消息。返回类型为id对象类型,参数是未处理的消息SEL

那么我们用代码验证下:

@implementation LGPerson

- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"%s -- %@", __func__, NSStringFromSelector(aSelector));
    return [super forwardingTargetForSelector:aSelector];
}

@end

运行工程,控制台输出如下:

image.png
虽然最终报错,但是报错前确实进入了[LGPerson forwardingTargetForSelector:] -- sayHello,说明系统确实调用了forwardingTargetForSelector方法。

我们再修改代码,让其完成处理消息转发:
先声明一个类LGStudent

@interface LGStudent : LGPerson

@end
//----------------分割线------------------
@implementation LGStudent

- (void)sayHello {
    NSLog(@"%s", __func__);
}

@end
//----------------调用处------------------
#import "LGStudent.h"

@implementation LGPerson

- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"%s -- %@", __func__, NSStringFromSelector(aSelector));
    
    return [LGStudent alloc];
}

@end
//----------------分割线------------------
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *p = [LGPerson alloc];
        [p sayHello];
        
        NSLog(@"Hello, World!");
    }
    return 0;
}

控制台输出:

2020-09-25 16:22:33.339387+0800 003-cache_t脱离源码环境分析[11476:2659115] -[LGPerson forwardingTargetForSelector:] -- sayHello
2020-09-25 16:22:33.339755+0800 003-cache_t脱离源码环境分析[11476:2659115] -[LGStudent sayHello]
2020-09-25 16:22:33.339797+0800 003-cache_t脱离源码环境分析[11476:2659115] Hello, World!
Program ended with exit code: 0

消息成功发送给了类LGStudent处理,没有报错!在forwardingTargetForSelector这一步就完成了消息转发,后面不会调用methodSignatureForSelector,这也说明了forwardingTargetForSelector会优先被系统调用。这就是所谓的快速转发流程!

2.2 慢速转发

剩下的方法是methodSignatureForSelector,既然有快速转发,对应的肯定有慢速转发,那么它就是慢速转发。还是先看官方文档说明

2.2.1 事务处理

forwardingTargetForSelector 快速转发
methodSignatureForSelector 慢速转发,查看官方说明文档,发现与NSInvocation搭配使用-(void)forwardInvocation

image.png
方法描述大致是:返回一个NSMethodSignature方法签名对象,去处理这个未识别的消息。

再看下类NSMethodSignature

@interface NSMethodSignature : NSObject

+ (nullable NSMethodSignature *)signatureWithObjCTypes:(const char *)types;

@property (readonly) NSUInteger numberOfArguments;
- (const char *)getArgumentTypeAtIndex:(NSUInteger)idx NS_RETURNS_INNER_POINTER;

@property (readonly) NSUInteger frameLength;

- (BOOL)isOneway;

@property (readonly) const char *methodReturnType NS_RETURNS_INNER_POINTER;
@property (readonly) NSUInteger methodReturnLength;

@end

看来我们只有通过这个类构造方法去创建方法签名对象`NSMethodSignature,还是查查这个方法的官方文档说明

image.png

参数是一个method arguments列表,再在官网文档搜索下关键词method arguments

image.png

红框处点击查看详情:


image.png

终于找到了方法参数的说明表格:


image.png

接着我们试着代码验证:

@implementation LGPerson

- (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@:"];
}

@end

运行看看:


image.png

还是报错了,按照文档说明,既然处理了这一步,应该不会报错才对,再回去看看methodSignatureForSelector的文档,拉到最下面

image.png

需配合这个方法使用,再看看forwardInvocation

image.png
交给了一个NSInvocation对象去处理。再改下代码:
@implementation LGPerson

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

@end

运行看看:

image.png
成功了!
我们再仔细看看NSInvocation
NSInvocation.png
有两个和方法关联的属性:target 和 selector,那么我们明白了,它能指定消息处理的targe接收者和 selector方法指针,接着我们代码再次验证:
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"%s -- %@", __func__, anInvocation);
    anInvocation.target = [LGStudent alloc];
    anInvocation.selector = @selector(sayHello);
    [anInvocation invoke];
}
image.png

完美解决,大功告成!

总结

综上所述,我们通过日志打印,发现了消息转发的入口方法,通过官网文档,一步一步用代码验证消息的转发流程,最终验证成功!现在总结一下消息转发的大致流程,流程图如下:


消息转发流程.jpg
上一篇 下一篇

猜你喜欢

热点阅读