iOS底层 – 消息转发

上一篇中我们探索了消息发送找不到方法的一个流程动态方法决议,本篇探索动态方法决议也找不到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的使用

  1. main.m文件里,通过extern声明instrumentObjcMessageSends方法。
  2. 在调用方法前打开日志,调用方法后关闭日志。代码如下:
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.jpg

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

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

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

反编译方式探索

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

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

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

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            JSPerson *person = [JSPerson alloc];
            [person sayNB];
        }
        return 0;
    }
    复制代码

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

1625404761976.jpg

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

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

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

1625405481311.jpg

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

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

1625405841994.jpg

1625406148399.jpg

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

Hopperc菜单示意图.jpg

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

1625407414541.jpg

  1. goto loc_64a67查看慢速转发

1625407939155.jpg

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

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

消息转发实例

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

快速消息转发

  • 我们先定义一个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类里实现methodSignatureForSelectorforwardInvocation,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的流程探索完了。

  1. 汇编代码快速查找缓存
  2. loopUpImpForward慢速递归查找类以及父类(包括缓存)的方法列表
  3. 动态方法解析处理消息
  4. 快速消息转发流程
  5. 慢速消息转发流程
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享