0%

iOS底层探索 - 消息转发

上一篇中我们探索了消息发送找不到方法的一个流程动态方法决议,本篇探索动态方法决议也找不到imp的下一步,也就是消息转发

和前面的流程不同的是,消息转发的源码并不在libobjc库里,而是在CFFoundtion框架里,CFFoundtion框已经开源的代码里没有找到消息转发的内容,所以我们必须找其它方式探索这个流程。

通过instrumentObjcMessageSends打印消息日志方式

instrumentObjcMessageSends的由来

通过方法调用这条链路lookUpImpOrForward --> log_and_fill_cache --> logMessageSend,我们在logMessageSend方法源码下方找到instrumentObjcMessageSends的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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目录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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的使用

  1. main.m文件里,通过extern声明instrumentObjcMessageSends方法。
  2. 在调用方法前打开日志,调用方法后关闭日志。代码如下:
1
2
3
4
5
6
7
8
9
10
11
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;
}
  1. 运行代码,查看日志文件

1625403519834

通过日志文件可以清楚的看到都执行了哪些方法:

  • resolveInstanceMethod方法,即动态方法决议
  • forwardingTargetForSelector方法,即快速消息转发
  • methodSignatureForSelector方法,即慢速消息转发
  • resolveInstanceMethod方法。即第二次动态方法决议
  • 最后执行doesNotRecognizeSelector,抛出异常。

这种方式我们就可以清楚的看到方法的调用流程,下面我们换一种方式验证一下。

反编译方式探索

Hopper是一款帮助我们静态分析可执行文件的工具。有了工具后我们还缺少两个东西才能继续探索:

  1. 反编译之后,搜索代码的关键字

    我们执行运行下面的代码,注意:sayNB方法是没有实现的

    1
    2
    3
    4
    5
    6
    7
    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    JSPerson *person = [JSPerson alloc];
    [person sayNB];
    }
    return 0;
    }

    运行之后会直接崩溃,我们使用bt命令查看调用栈信息

    1625404761976

    可以看到这里方法调用的起点是CoreFoundation_forwarding_prep_0__ + 120`,所以我们搜索反编译代码的关键字就是forwarding_prep_0

  2. 怎么获取我们需要反编译的可执行文件

    通过1步,我们知道__forwarding_prep_0___CoreFoundation框架中,我们通过lldbimage list命令,找到CoreFoundation可执行文件的位置。

    1625405481311

    通过上图中目录我们找到CoreFoundation文件。

  3. 前面两个必要条件明确之后,我们打开Hooper软件,选择Try The Demo(主要因为软件太贵,土豪请直接购买正版),然后将上一步的可执行文件拖入Hooper进行反编译,选择x86(64 bit)

    1625405841994

    1625406148399

    Hooper软件我们用到的工具栏的示意图如下图:

    Hopperc菜单示意图

  4. 搜索框位置搜索__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
    25
    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;
    }
  5. 继续看____forwarding___方法的伪代码,首先是判断是否实现快速转发方法forwardingTargetForSelector,如果没有跳转到loc_64a67慢速转发流程

    1625407414541

  6. goto loc_64a67查看慢速转发

    1625407939155

  7. 如果没有实现则跳转,直接报错。

通过使用反编译的方式我们也验证了消息转发的流程。

消息转发实例

在前面的例子中,我们补充一下消息转发的部分.

快速消息转发

  • 我们先定义一个JSProxy类,它实现了sayNB方法
1
2
3
4
5
6
7
8
9
10
11
12
@interface JSPerson : NSObject

- (void)sayNB;

@end
@implementation JSProxy

- (void)sayNB{
NSLog(@"%@ - %s",self , __func__);
}

@end
  • JSPerson类中添加forwardingTargetForSelector方法
1
2
3
4
5
6
7
#import "JSProxy.h"
@implementation JSPerson
- (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"forwardingTargetForSelector :%@-%@",self,NSStringFromSelector(aSelector));
return [JSProxy alloc];
}
@end
  • 运行代码程序可以正常运行,打印log如下:
1
2
3
4
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类里实现methodSignatureForSelectorforwardInvocation,forwardingTargetForSelector方法返回nil标示不进行快速消息转发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@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

我们运行代码,发现运行正常,打印结果如下:

1
2
3
4
5
6
7
8
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的流程探索完了。

  1. 汇编代码快速查找缓存
  2. loopUpImpForward慢速递归查找类以及父类(包括缓存)的方法列表
  3. 动态方法解析处理消息
  4. 快速消息转发流程
  5. 慢速消息转发流程