前言
我们在上一篇文章 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
能够被调用执行到的原因。
总结
消息转发流程图: