上一篇中我们探索了消息发送找不到方法的一个流程动态方法决议
,本篇探索动态方法决议
也找不到imp
的下一步,也就是消息转发
。
和前面的流程不同的是,消息转发
的源码并不在libobjc
库里,而是在CFFoundtion
框架里,CFFoundtion
框已经开源的代码里没有找到消息转发的内容,所以我们必须找其它方式探索这个流程。
通过instrumentObjcMessageSends打印消息日志方式
instrumentObjcMessageSends的由来
通过方法调用这条链路lookUpImpOrForward --> log_and_fill_cache --> logMessageSend
,我们在logMessageSend
方法源码下方找到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);
//赋值是否需要打印message log
objcMsgLogEnabled = enable;
}
复制代码
log
文件存储的路径我们从logMessageSend
方法里可以看到,为/tmp/msgSends
目录:
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;
}
复制代码
instrumentObjcMessageSends的使用
- 在
main.m
文件里,通过extern
声明instrumentObjcMessageSends
方法。 - 在调用方法前打开日志,调用方法后关闭日志。代码如下:
extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
@autoreleasepool {
instrumentObjcMessageSends(YES);
JSPerson *person = [JSPerson alloc];
[person saySomething];
instrumentObjcMessageSends(NO);
}
return 0;
}
复制代码
- 运行代码,查看日志文件
通过日志文件可以清楚的看到都执行了哪些方法:
resolveInstanceMethod
方法,即动态方法决议
forwardingTargetForSelector
方法,即快速消息转发
methodSignatureForSelector
方法,即慢速消息转发
resolveInstanceMethod
方法。即第二次动态方法决议
- 最后执行
doesNotRecognizeSelector
,抛出异常。
这种方式我们就可以清楚的看到方法的调用流程,下面我们换一种方式验证一下。
反编译方式探索
Hopper是一款帮助我们静态分析可执行文件的工具。有了工具后我们还缺少两个东西才能继续探索:
-
反编译之后,搜索代码的
关键字
我们执行运行下面的代码,注意:
sayNB
方法是没有实现的int main(int argc, const char * argv[]) { @autoreleasepool { JSPerson *person = [JSPerson alloc]; [person sayNB]; } return 0; } 复制代码
运行之后会直接崩溃,我们使用
bt
命令查看调用栈信息
可以看到这里方法调用的起点是CoreFoundation
_forwarding_prep_0__ + 120`,所以我们搜索反编译代码的关键字就是forwarding_prep_0
-
怎么获取我们需要反编译的可执行文件
通过
1
步,我们知道__forwarding_prep_0___
在CoreFoundation
框架中,我们通过lldb
的image list
命令,找到CoreFoundation
可执行文件的位置。
通过上图中目录我们找到CoreFoundation
文件。
-
前面两个必要条件明确之后,我们打开
Hooper
软件,选择Try The Demo
(主要因为软件太贵,土豪请直接购买正版),然后将上一步的可执行文件拖入Hooper
进行反编译,选择x86(64 bit)
Hooper
软件我们用到的工具栏的示意图如下图:
-
在
搜索框
位置搜索__forwarding_prep_0___
,选择伪代码
视图,发现和我们打印的堆栈信息一致,调用了____forwarding___
方法int ___forwarding_prep_0___(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5) { var_20 = rax; var_30 = zero_extend_64(xmm7); var_40 = zero_extend_64(xmm6); var_50 = zero_extend_64(xmm5); var_60 = zero_extend_64(xmm4); var_70 = zero_extend_64(xmm3); var_80 = zero_extend_64(xmm2); var_90 = zero_extend_64(xmm1); var_A0 = zero_extend_64(xmm0); var_A8 = arg5; var_B0 = arg4; var_B8 = arg3; var_C0 = arg2; var_C8 = arg1; ///和我们打印的调用栈一致 rax = ____forwarding___(&var_D0, 0x0); if (rax != 0x0) { rax = *rax; } else { rax = objc_msgSend(var_D0, var_C8); } return rax; } 复制代码
-
继续看
____forwarding___
方法的伪代码,首先是判断是否实现快速转发
方法forwardingTargetForSelector
,如果没有跳转到loc_64a67
走慢速转发流程
- goto
loc_64a67
查看慢速转发
- 如果没有实现则跳转,直接报错。
通过使用反编译的方式我们也验证了消息转发
的流程。
消息转发实例
在前面的例子中,我们补充一下消息转发
的部分.
快速消息转发
- 我们先定义一个
JSProxy
类,它实现了sayNB
方法
@interface JSPerson : NSObject
- (void)sayNB;
@end
@implementation JSProxy
- (void)sayNB{
NSLog(@"%@ - %s",self , __func__);
}
@end
复制代码
- 在
JSPerson
类中添加forwardingTargetForSelector
方法
#import "JSProxy.h"
@implementation JSPerson
- (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"forwardingTargetForSelector :%@-%@",self,NSStringFromSelector(aSelector));
return [JSProxy alloc];
}
@end
复制代码
- 运行代码程序可以正常运行,打印
log
如下:
2021-07-04 22:24:49.089260+0800 ResolveMethodTest[7682:442095] resolveInstanceMethod :JSPerson-sayNB
2021-07-04 22:24:49.090863+0800 ResolveMethodTest[7682:442095] resolveInstanceMethod :JSPerson-encodeWithOSLogCoder:options:maxLength:
2021-07-04 22:24:49.091431+0800 ResolveMethodTest[7682:442095] forwardingTargetForSelector :<JSPerson: 0x600000010160>-sayNB
2021-07-04 22:24:49.091571+0800 ResolveMethodTest[7682:442095] <JSProxy: 0x6000000080d0> - -[JSProxy sayNB]
复制代码
慢速消息转发
我们在上面例子基础上,在JSPerson
类里实现methodSignatureForSelector
和forwardInvocation
,forwardingTargetForSelector
方法返回nil
标示不进行快速消息转发
。
@implementation JSPerson
- (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"forwardingTargetForSelector :%@-%@",self,NSStringFromSelector(aSelector));
return nil;
}
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSLog(@"methodSignatureForSelector :%@-%@",self,NSStringFromSelector(aSelector));
if (aSelector == @selector(sayNB)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
-(void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"%@ - %s",self , __func__);
}
@end
复制代码
我们运行代码,发现运行正常,打印结果如下:
2021-07-04 22:34:32.163137+0800 ResolveMethodTest[7780:447311] resolveInstanceMethod :JSPerson-sayNB
2021-07-04 22:34:32.163803+0800 ResolveMethodTest[7780:447311] resolveInstanceMethod :JSPerson-encodeWithOSLogCoder:options:maxLength:
2021-07-04 22:34:32.164673+0800 ResolveMethodTest[7780:447311] forwardingTargetForSelector :<JSPerson: 0x600000008070>-sayNB
2021-07-04 22:34:32.165289+0800 ResolveMethodTest[7780:447311] resolveInstanceMethod :JSPerson-encodeWithOSLogCoder:options:maxLength:
2021-07-04 22:34:32.165436+0800 ResolveMethodTest[7780:447311] methodSignatureForSelector :<JSPerson: 0x600000008070>-sayNB
2021-07-04 22:34:32.165551+0800 ResolveMethodTest[7780:447311] resolveInstanceMethod :JSPerson-_forwardStackInvocation:
2021-07-04 22:34:32.165634+0800 ResolveMethodTest[7780:447311] resolveInstanceMethod :JSPerson-encodeWithOSLogCoder:options:maxLength:
2021-07-04 22:34:32.165986+0800 ResolveMethodTest[7780:447311] <JSPerson: 0x600000008070> - -[JSPerson forwardInvocation:]
复制代码
这里可能有人对添加forwardInvocation
方法有因为,可以查看苹果官方文档查看原因
总结
到这里我们就就把整个objc_msgSend
的流程探索完了。
- 汇编代码快速查找缓存
loopUpImpForward
慢速递归查找类以及父类(包括缓存)的方法列表- 动态方法解析处理消息
- 快速消息转发流程
- 慢速消息转发流程