上一篇中我们探索了消息发送找不到方法的一个流程动态方法决议
,本篇探索动态方法决议
也找不到imp
的下一步,也就是消息转发
。
和前面的流程不同的是,消息转发
的源码并不在libobjc
库里,而是在CFFoundtion
框架里,CFFoundtion
框已经开源的代码里没有找到消息转发的内容,所以我们必须找其它方式探索这个流程。
通过instrumentObjcMessageSends打印消息日志方式
instrumentObjcMessageSends的由来
通过方法调用这条链路lookUpImpOrForward --> log_and_fill_cache --> logMessageSend
,我们在logMessageSend
方法源码下方找到instrumentObjcMessageSends
的实现:
1 | void instrumentObjcMessageSends(BOOL flag) |
log
文件存储的路径我们从logMessageSend
方法里可以看到,为/tmp/msgSends
目录:
1 | bool logMessageSend(bool isClassMethod, |
instrumentObjcMessageSends的使用
- 在
main.m
文件里,通过extern
声明instrumentObjcMessageSends
方法。 - 在调用方法前打开日志,调用方法后关闭日志。代码如下:
1 | extern void instrumentObjcMessageSends(BOOL flag); |
- 运行代码,查看日志文件
通过日志文件可以清楚的看到都执行了哪些方法:
resolveInstanceMethod
方法,即动态方法决议
forwardingTargetForSelector
方法,即快速消息转发
methodSignatureForSelector
方法,即慢速消息转发
resolveInstanceMethod
方法。即第二次动态方法决议
- 最后执行
doesNotRecognizeSelector
,抛出异常。
这种方式我们就可以清楚的看到方法的调用流程,下面我们换一种方式验证一下。
反编译方式探索
Hopper是一款帮助我们静态分析可执行文件的工具。有了工具后我们还缺少两个东西才能继续探索:
反编译之后,搜索代码的
关键字
我们执行运行下面的代码,注意:
sayNB
方法是没有实现的1
2
3
4
5
6
7int 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___
方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25int ___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
方法
1 | @interface JSPerson : NSObject |
- 在
JSPerson
类中添加forwardingTargetForSelector
方法
1 | #import "JSProxy.h" |
- 运行代码程序可以正常运行,打印
log
如下:
1 | 2021-07-04 22:24:49.089260+0800 ResolveMethodTest[7682:442095] resolveInstanceMethod :JSPerson-sayNB |
慢速消息转发
我们在上面例子基础上,在JSPerson
类里实现methodSignatureForSelector
和forwardInvocation
,forwardingTargetForSelector
方法返回nil
标示不进行快速消息转发
。
1 | @implementation JSPerson |
我们运行代码,发现运行正常,打印结果如下:
1 | 2021-07-04 22:34:32.163137+0800 ResolveMethodTest[7780:447311] resolveInstanceMethod :JSPerson-sayNB |
这里可能有人对添加forwardInvocation
方法有因为,可以查看苹果官方文档查看原因
总结
到这里我们就就把整个objc_msgSend
的流程探索完了。
- 汇编代码快速查找缓存
loopUpImpForward
慢速递归查找类以及父类(包括缓存)的方法列表- 动态方法解析处理消息
- 快速消息转发流程
- 慢速消息转发流程