前言
我们在上一篇文章 OC - 底层动态方法决议 对动态方法决议 有了初步了解。今天我们将一起探索消息发送的最后一个流程 消息的转发。还有朋友没有下载 objc 源码的可以在这里进行下载。今天我们需要准备 CF 源码 以及反汇编工具 Hopper 和 ida。
消息的转发
在 lookUpImpOrForward 慢查询中,从元类中到了 imp,就会进入到 done 流程中。在done中判断共享缓存然后走到log_and_fill_cache函数中。
在 log_and_fill_cache 函数中添加缓存。
cls->cache.insert(sel, imp, receiver); 这行代码我们已经非常熟悉了。但是这里在执行它之前有一个判断。如果满足 slowpath(objcMsgLogEnabled && implementer) 这么一个条件就会执行 logMessageSend 往 /tmp/msgSends-%d 路径中写入一些数据。已知 implementer 是当前的类,所以只要 objcMsgLogEnabled 为 YES 时就会执行。接下我们看看 objcMsgLogEnabled 是在哪里进行赋值的。全局搜索。
在这个地方有一个默认赋值为
false。
通过全局搜索,在
instrumentObjcMessageSends 函数中对 objcMsgLogEnabled 进行了赋值。所以我们就可以把这个方法外部扩充,然后传入 YES or NO。就能进行监听!
extern void instrumentObjcMessageSends(BOOL flag);
然后运行代码。
#import <Foundation/Foundation.h>
#import "TPerson.h"
extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
@autoreleasepool {
instrumentObjcMessageSends(YES);
TPerson * tp = [TPerson alloc];
[tp formatPerson];
instrumentObjcMessageSends(NO);
NSLog(@"Hello, World!");
}
return 0;
}
在 /tmp/msgSends-%d 路径下就有了这么一个文件。
打开这个文件 msgSends-2060。
分析这个文件,发现我们需要关注的只有头部这几个东西。
resolveInstanceMethod 这个我们在 OC - 底层动态方法决议 已经了解了。forwardingTargetForSelector 、methodSignatureForSelector 、doesNotRecognizeSelector 是什么我们不知道,接下来我们就一一进行探究分析。
forwardingTargetForSelector(快速转发流程)
在Xcode 中打开 command + shift + o ,进搜索。
苹果在这个地方就做了详细的解释。接下来我们就在 TPerson 中进行实现。然后运行
#import "TPerson.h"
#import "TStudent.h"
@implementation TPerson
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"%s -- %@",__func__ ,NSStringFromSelector(aSelector));
return [super forwardingTargetForSelector:aSelector];
}
@end
输出:
-[TPerson forwardingTargetForSelector:] -- formatPerson
-[TPerson formatPerson]: unrecognized selector sent to instance 0x100526860
unrecognized 崩溃了。 我们 TStudent 中也添加了一个 formatPerson的方法,并且实现了方法。那么我们能不能把 TPerson 接收的消息转发给 TStudent 呢?接下来我们一起验证一下。
#import "TPerson.h"
#import "TStudent.h"
@implementation TPerson
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"%s -- %@",__func__ ,NSStringFromSelector(aSelector));
return [TStudent alloc];
}
@end
输出:
-[TPerson forwardingTargetForSelector:] -- formatPerson
-[TStudent formatPerson]
程序正常运行。这个地方并没有像动态方法那样需要处理的东西很多,而且臃肿。
那么我们为啥不将 formatPerson 方法在 TPerson 中实现呢?反而要在 TStudent 中实现呢?是不是有毛病呢?因为这是一个 通过 sel 找 imp 的过程。首先是通过 objc_msgSend 从 cache 缓存中查找,如果没有查找到。然后再慢速查找里面通过查找 methodlist 查找。如果还是没有找到。说明在内存中这个方法根本不存在。就可以通过 动态方法决议 进行补救,只需要提供一个能够实现的就可以。
只要走到 - (id)forwardingTargetForSelector:(SEL)aSelector 这个方法中,我们就往 TStudent 中添加方法。这样就能保证 TPerson 的安全运行。那么如果 TStudent 也没有实现这个方法呢。程序就会继续报错,然后走到 methodSignatureForSelector 中。
methodSignatureForSelector(慢速转发流程)
接下来我们搜索 methodSignatureForSelector 查看官方给出的解释。
接下来我们就代码实现一下,运行程序。
#import "TPerson.h"
#import "TStudent.h"
@implementation TPerson
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"%s -- %@",__func__ ,NSStringFromSelector(aSelector));
return [TStudent alloc];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSLog(@"%s -- %@",__func__ ,NSStringFromSelector(aSelector));
return [super methodSignatureForSelector:aSelector];
}
@end
输出:
-[TPerson forwardingTargetForSelector:] -- formatPerson
-[TStudent formatPerson]: unrecognized selector sent to instance 0x100746d00
程序崩溃了。通过崩溃日志我们发现程序并没有走到 methodSignatureForSelector 方法中。那么这是为什么呢?因为 TStudent 中并没有实现 formatPerson 方法。所有在 forwardingTargetForSelector 方法中返回 [TStudent alloc] 不对的。所有这个地方不能同时调用 forwardingTargetForSelector 方法。在官方对 methodSignatureForSelector 的解释中,我们得知 methodSignatureForSelector 方法是需要搭配 forwardInvocation: 方法一起使用的,而且是在 方法中进行解读的。注意:这个地方要对签名进行处理。
#import "TPerson.h"
#import "TStudent.h"
@implementation TPerson
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSLog(@"%s -- %@",__func__ ,NSStringFromSelector(aSelector));
if (aSelector == @selector(formatPerson)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"%s -- %@ -- %@",__func__ ,anInvocation.target, NSStringFromSelector(anInvocation.selector));
}
@end
输出:
-[TPerson methodSignatureForSelector:] -- formatPerson
-[TPerson forwardInvocation:] -- <TPerson: 0x10074e440> -- formatPerson
Hello, World!
程序正常运行。这就是慢速转发流程。
查看 forwardInvocation 官方解释
对 forwardInvocation 进行操作。根据自身的需要进行相关的处理。
- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"%s -- %@ -- %@",__func__ ,anInvocation.target, NSStringFromSelector(anInvocation.selector));
TStudent * ts = [TStudent alloc];
if ([self respondsToSelector:anInvocation.selector]) {
[anInvocation invoke];
} else if ([ts respondsToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:ts];
} else {
NSLog(@" %s -- %@", __func__ , NSStringFromSelector(anInvocation.selector));
}
}
输出:
-[TPerson methodSignatureForSelector:] -- formatPerson
-[TPerson forwardInvocation:] -- <TPerson: 0x1060474a0> -- formatPerson
-[TStudent formatPerson]
Hello, World!
Hopper 反汇编工具
当我们在遇到 unrecognized 崩溃的时候。我们可以通过反汇编可执行文件 CoreFoundation 进行查找。
我们可以通过 image list 获取所有的镜像文件,找到 CoreFoundation 的路径。
接下来我们就在 Hopper 中打开 CoreFoundation。
然后我们就全局搜索 __forwarding_prep_0。
发现在 CoreFoundation 中 __forwarding_prep_0 只有一个地方在调用。在 __forwarding_prep_0 方法中,调用了 ____forwarding___ 方法。接下来我们查看一下 ____forwarding___ 方法。
如果没有实现了 forwardingTargetForSelector: 方法就会走到 loc_64a67 里面。
如果 forwardingTargetForSelector: 方法的返回值为空就会走到 loc_64a67 里面。
在
loc_64a67 中判断是否有僵尸对象。
判断是否能够响应
methodSignatureForSelector: 方法。如果能够响应且返回值不等于空,就继续走r14,等于空就走到 loc_64dd7。显然这个地方不等于空。
接下来就继续往下走
然后就会判断是否能够响应
_forwardStackInvocation: 方法。 我们在工程中无法使用_forwardStackInvocation: 这个方法,显然这个方法是系统提供的一个内部层面的方法,没有对外界进行暴露。如果能够响应就会继续往下走调用 [NSInvocation requiredStackSizeForSignature:r12]; 方法。如果不能响应就直接报错。
这就是为什么在慢速转发流程中 forwardInvocation 能够被调用执行到的原因。
总结
消息转发流程图: