iOS九阴真经:十八、消息传递:消息转发流程

848 阅读7分钟

在 Objective-C 中,当我们调用一个方法的本质是消息传递,那么消息传递在经过快速查找->慢速查找->动态方法解析三个流程之后,还是没有找到该方法的实现。那么接下来会进入下一个流程,消息转发流程。

一、消息转发流程的引入

1. instrumentObjcMessageSends 函数介绍

在 objc 源码的 objc_class.mm 文件中,有一个 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;
}

flag 为 YES 时,刷新所有方法缓存,并且将同步到日志文件。那么日志文件存放在哪里呢?在 instrumentObjcMessageSends 函数的上方,有一个 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;
}

logMessageSend 函数的实现大多是一些日志的格式化输出。当调用 logMessageSend 函数的时候,会将日志文件存到 /tmp/ 路经下,并且文件名以 msgSends- 开头。

2. logMessageSend 函数的由来

那么我为什么就这么肯定一定会走 logMessageSend 函数呢?还记得在慢速查找 - lookUpImpOrForward 函数的实现吗,在函数的实现,有一个 done: 流程,当找到 imp 时,会跳转进 done: 流程,然后调用 log_and_fill_cache 函数,对 imp 进行缓存。

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
    cls->cache.insert(sel, imp, receiver);
}

看到第一个判断,当 objcMsgLogEnabled && implementer 成立的时候,就会调用 logMessageSend 函数,而 objcMsgLogEnabled ,不就是在 instrumentObjcMessageSends 函数内部赋值的吗。所以,instrumentObjcMessageSends 函数就是一个类似开启日志缓存的开关。

3. 测试 instrumentObjcMessageSends 函数输出日志文件

接下来我们来测试一下,测试代码如下:

extern void instrumentObjcMessageSends(BOOL flag);

@interface SHPerson : NSObject
- (void)helloWorld;
@end
@implementation SHPerson
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        SHPerson *p = [[SHPerson alloc] init];
        instrumentObjcMessageSends(YES);
        [p helloWorld];
        instrumentObjcMessageSends(NO);
    }
    return 0;
}

需要注意的是,在测试的时候,不要用源码工程来测试,否则 msgSends- 文件会没有内容。查看 /tmp/ 路径下是否有 msgSends- 开头的文件。

msgSends-文件路径.png

好家伙,果然有,我们来看一下文件中的日志。

msgSends-日志内容.png

当我们进行动态方法解析之后,仍然没有找到方法的实现,这个时候系统还是会给开发者一次机会,那就是进行消息转发流程。如图中所示,消息转发流程主要有两个方法,分别为 forwardingTargetForSelector:methodSignatureForSelector:

二、消息转发流程

消息转发流程是怎么转发呢?我们先来看看 forwardingTargetForSelector: 方法和 methodSignatureForSelector: 方法怎么用。

1. 快速转发流程

forwardingTargetForSelector: 方法的返回值为 id,参数为 aSelector。那么根据官方的注解,我个人的理解为,当实现这个方法,可以对 aSelector 进行转发,接收的对象为 id 类型,也就是任意对象。当我们返回接收的对象时,接收的对象会对 aSelector 继续进行查找,也就是重复前面所讲的消息传递的几个流程。

我们举个例子,现在有两个对象,分别为 SHPersonSHAnimal,我们在 SHPerson 中声明 run 方法,但不实现,并且实现 forwardingTargetForSelector: 方法。在 SHAnimal 中实现一个 run 方法。具体的代码如下:

@interface SHAnimal : NSObject
@end
@implementation SHAnimal
- (void)run {
    NSLog(@"%s", __func__);
}
@end
@interface SHPerson : NSObject
- (void)run;
@end
@implementation SHPerson
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(run)) {
        NSLog(@"%s",__func__);
        return [SHAnimal alloc];
    }
    return [super forwardingTargetForSelector:aSelector];
}
@end
SHPerson *p = [[SHPerson alloc] init];
[p run];
打印结果:
2021-12-27 16:28:14.862557+0800 06-消息传递:消息转发-01[72288:2241695] -[SHPerson forwardingTargetForSelector:]
2021-12-27 16:28:14.862844+0800 06-消息传递:消息转发-01[72288:2241695] -[SHAnimal run]

当我们在 SHPerson 没有实现 run 方法的时候,除了可以在动态方法解析那一流程做处理之外,还可以在 forwardingTargetForSelector: 方法中做处理。就如同打印的结果,SHPerson 没有实现 run ,我们手动的让它去 SHAnimal 对象里找。

SHAnimal 对象就是当前消息转发的接收者,很多人也称它为备用接收者,或者称为备胎。

2. 慢速转发流程

当我们在 forwardingTargetForSelector: 方法做处理的时候,总会觉得奇奇怪怪的。如果 SHAnimal 也不实现 run 方法,程序一样会崩溃,毕竟只是备胎😂,所以我们不想在 forwardingTargetForSelector: 中做处理,那么就开始进入到下一个流程,叫慢速转发流程,也就是实现 methodSignatureForSelector: 方法,在 methodSignatureForSelector: 方法中做转发的处理。

1. methodSignatureForSelector:

methodSignatureForSelector: 方法需要返回一个 NSMethodSignature 对象,也就是方法签名。需要注意的是,methodSignatureForSelector:forwardingTargetForSelector: 不能同时存在,否则就只走到 forwardingTargetForSelector: ,不会走到 methodSignatureForSelector:

代码如下:

@interface SHPerson : NSObject
- (void)run;
@end
@implementation SHPerson
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(run)) {
        NSLog(@"%s",__func__);
        NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
        return signature;
    }
    return [super methodSignatureForSelector:aSelector];
}

慢速转发崩溃.png

我们把代码跑起来后,虽然调用了 methodSignatureForSelector:  方法,但程序还是崩了。难道 methodSignatureForSelector: 方法不能解决吗,我在看 methodSignatureForSelector: 方法的文档说明的时候,注意到了 forwardInvocation: 方法。

2. forwardInvocation:

在实现 methodSignatureForSelector: 方法的同时,也必须创建 NSInvocation 对象。我理解的大概意思是,methodSignatureForSelector:forwardInvocation: 必须一起实现,因为实现了 forwardInvocation: 方法,会去创建 NSInvocation 对象,并且将 NSInvocation 对象作为参数传到 forwardInvocation: 方法。

那么,我们实现 forwardInvocation: 方法,并重新运行。

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"%s",__func__);
}

forwardInvocation 打印.png

实现了 forwardInvocation: 方法后,果然不崩了,并且还打印了 methodSignatureForSelector:forwardInvocation:

那为什么实现了 forwardInvocation: 方法之后,不用做任何的处理,程序都不会崩溃呢。下面是我翻译官方文档对 forwardInvocation: 的说明。

  • 当一个对象收到一条没有相应方法的消息时,运行时系统会给接收者一个机会将消息委托给另一个接收者。它通过创建一个表示消息的 NSInvocation 对象并向接收者发送一个 forwardInvocation: 消息来委托消息,该消息包含这个 NSInvocation 对象作为参数。然后,接收者的 forwardInvocation: 方法可以选择将消息转发到另一个对象。 (如果该对象也无法响应消息,它也将有机会转发它。)

  • forwardInvocation: 消息因此允许一个对象与其他对象建立关系,对于某些消息,这些对象将代表它行事。从某种意义上说,转发对象能够“继承”将消息转发到的对象的某些特征。

  • 要响应您的对象本身无法识别的方法,除了 forwardInvocation: 之外,您还必须覆盖 methodSignatureForSelector:

  • 转发消息的机制使用从 methodSignatureForSelector: 获得的信息来创建要转发的 NSInvocation 对象。您的覆盖方法必须为给定的选择器提供适当的方法签名,通过预先制定一个方法或通过向另一个对象询问一个方法。

forwardInvocation: 方法的实现有两个任务:

  1. 定位可以响应 anInvocation 中编码的消息的对象。该对象不必对所有消息都相同。
  2. 使用调用将消息发送到该对象。anInvocation 将保存结果,运行时系统将提取该结果并将其传递给原始发送者。

那么,什么意思呢,我们来看一段 forwardInvocation: 的简单实现。

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(run)) {
        NSLog(@"%s",__func__);
        NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
        return signature;
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"%s",__func__);

    SEL aSelector = [anInvocation selector];
    SHAnimal *forward = [SHAnimal alloc];

    if ([forward respondsToSelector:aSelector]) {
        [anInvocation invokeWithTarget:forward];
    }else {
        [super forwardInvocation:anInvocation];
    }
}

forwardInvocation实现打印.png

通过这段代码和打印,正好就是印证了官方文档的注释。forwardInvocation: 实现之后,可以在方法中通过 NSInvocation 对象进行最后的消息转发处理。

NSInvocation 相当于事物,你只需要告诉它,是否要进行消息转发,需要的话,就像上面的例子。不需要进行转发的话,NSInvocation 对象会很乖,什么也不管,但是不会导致程序崩溃,因为只要实现了 methodSignatureForSelector: ,返回方法签名,并且创建 NSInvocation 对象,就不会崩溃。

forwardInvocation: 会帮我们创建一个 NSInvocation 对象,并且把这个对象传给我们,让我们通过 NSInvocation 对象进行最后的消息转发。