iOS底层之消息转发

594 阅读6分钟

前言

之前讲到了消息发送的快速查找和慢速查找,如果都没有找到就会进入消息动态决议,那么消息动态决议了之后就会直接报错了吗?接下来一起看看。

消息转发

lookUpImpOrForward()代码里,消息动态决议下面的代码是这样的。

image.png

虽然看到消息动态决议的地方是return了,但是在resolveMethod_locked()实现里面可以看到又调用了lookUpImpOrForward(),并且由于behavior的变化,不会再调用resolveMethod_locked(),所以会来到log_and_fill_cache()

image.png

在这个函数里,可以看到cls向缓存里插入sel和imp,这个之前也研究过了,但是注意到上面有一句logMessageSend(),这个是干什么的?来看看源码,全局搜索,找到实现的地方。

image.png

这个函数实现里可以看到系统创建了一个文件,并且记录了一些函数方法。
要执行这个函数,得判断(objcMsgLogEnabled && implementer)implementer是传入的cls,这个一般都是存在,那么就是objcMsgLogEnabled了,全局搜索看看它是怎么赋值的。

image.png image.png

可以看到objcMsgLogEnabled是默认为false的,并且会在instrumentObjcMessageSends()这个函数里重新赋值。那么只需要调用这个函数,就可以看看系统记录了些什么方法。
在非源码环境下调用试试。

image.png

我们创建了一个DDAnimal的文件,声明了一个类方法,但是没有实现,因为只需要看这个方法的调用,所以在调用结束后就把objcMsgLogEnabled设置为false。
到Finder里使用快捷键Command+Shift+G直接跳转到/tmp文件夹。

image.png

可以看到系统生成一个msgSends-71158的文件,打开看看。

image.png

瞬间一目了然,先是调用了resolveClassMethod:,然后又调用resolveInstanceMethod:,这个之前看到的类方法查找顺序是一样的。接着又调用了forwardingTargetForSelector:methodSignatureForSelector:,最后才调用了doesNotRecognizeSelector:
这几个方法只有forwardingTargetForSelector:methodSignatureForSelector:没有研究过,来看看这两个方法是什么。

这里还有几种情况:

  1. 如果实现了方法,那么就不会生成这个文件;
  2. 如果实现resolveInstanceMethod:,那么就不会记录forwardingTargetForSelector:methodSignatureForSelector:以及其后的方法和函数。

forwardingTargetForSelector:

这个方法是怎么用的呢,在源码里搜索也只是return nil,无法提供参考。对于这种不知用法和意义的方法,可以到Dash里去查询。

returns the object to which unrecognized messages should first be directed.
大致意思是返回未识别的方法应该指向的对象。

// 在DDAnimal.m文件里添加方法。
// 如果是类方法就返回要指向的类,如果是对象方法就返回要指向的实例对象。
+ (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
    return [DDCat class];
}

#import "DDCat.h"

@implementation DDCat

// 被指向的对象必须实现同名的类方法或者对象方法。
+ (void)run {
    NSLog(@"%s",__func__);
}

@end

打印结果

2021-07-07 18:12:12.522777+0800 res[98732:1778794] +[DDAnimal forwardingTargetForSelector:] - run
2021-07-07 18:12:12.523866+0800 res[98732:1778794] +[DDCat run]

可以看到成功打印了,并没有报错。
这个方法也叫做快速转发,主要功能是将方法返回的对象作为方法的新的接收对象。

methodSignatureForSelector:

同样的到Dash搜索用法。

Returns an NSMethodSignature object that contains a description of the method identified by a given selector.
大致意思是要返回一个NSMethodSignature对象,其包含了方法的标识符,也就是方法的类型。
并且该方法有一个关联方法forwardInvocation:,在Dash查找结果最下面可以看到。

看看代码实现。

// + (void)run;的类型是v@:,因为底层会将方法转换会默认有两个参数,一个是接收对象,一个sel(_cmd)。
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(run)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

// 在Dash看到有个关联方法,这个比方必须实现,可以在这里处理方法,也可以不处理。但是必须要有,不然崩溃。
+ (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"%@ -- %@", anInvocation.target, NSStringFromSelector(anInvocation.selector));
}

打印结果

image.png

可以看到没有再崩溃了,并且成功打印出了日志。
在看看怎么实现forwardInvocation:可以怎样实现。

+ (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"%@ -- %@", anInvocation.target, NSStringFromSelector(anInvocation.selector));
    if ([self respondsToSelector:anInvocation.selector]) {
        [anInvocation invoke];
    }else if ([DDCat respondsToSelector:anInvocation.selector]){
        [anInvocation invokeWithTarget:DDCat.class];
    }else{
        NSLog(@"%s -- %@", __func__, NSStringFromSelector(anInvocation.selector));
    }
}

打印结果,成功调用。

image.png

可以看出这个方法调用复杂,还要搭配着forwardInvocation:,效率也就相对较低。这一步也叫做慢速转发,如果慢速转发还是没有处理,那么大概率就会调用doesNotRecognizeSelector:,直接崩溃了。

消息转发逻辑探索

前面只是看到了方法调用的记录日志,大概猜出了这样一个流程,可是底层逻辑是怎么做的,我们不得而知,那么能不能探索到呢?
现在什么都不处理,仍然只是声明了一个类方法run,并没有实现,运行一下,看看堆栈信息里有没有什么可用的信息。
lldb执行bt指令,拿到堆栈信息。

image.png

可以看到在调用doesNotRecognizeSelector:之前有执行___forwarding____CF_forwarding_prep_0,也许这就是关键,全局搜索看看怎么实现的。搜索___forwarding___找不到任何信息,去掉下划线试一下。

image.png

这次虽然有结果,但是基本都是一些注释,根本没用,怎么办?在上面的堆栈信息里可以看到___forwarding___是在CoreFoundation,是否要去下载CoreFoundation源码看看。
一波操作后,找到源码,但是搜索没有结果。

image.png

几种方法都试过了,这个时候就只有再尝试反汇编查看了。

image.png

在iOS工程里随意处打个断点,然后lldb执行指令image list,可以看到本地的所有image镜像库文件。找到CoreFoundation。复制路径url,到finder里找到对应的库。然后用Hopper打开。
在Hopper中搜索___forwarding___,结果如下。

image.png

这个方法里面代码很多,但是不用担心,我们只需要记住我们的目标,那就是找到转发流程的代码逻辑。

消息转发逻辑.png

上图就是消息转发的代码逻辑,基本印证了我们之前的猜想。

消息转发流程

消息转发流程.png

上图是消息转发的流程。

总结

消息发送通过消息查找之后没有找到对应的方法,则会进入消息转发流程,消息转发又分快速转发和慢速转发。
快速转发只需要指定一个能够响应该方法的对象即可。
慢速转发则需要一个方法签名,然后再forwardInvocation:里进行处理,转发给可以响应的对象,当然也可以不处理,就这样结束了。