iOS探索底层-消息转发

849 阅读9分钟

前言

在之前的探索中,我们对整个消息发送的流程探索到了动态方法决议这一步,在探索的过程中,我们遇到了一些问题,那就是resolveInstanceMethod会调用两次

image.png 我们通过堆栈看到了两次调用之间的区别

image.png

  • 第一次调用的路径是main->_objc_msgSend_uncached->lookUpImpOrForward->resolveInstanceMethod。也就是系统在快速方法查找慢速方法查找之后,如果没有找到方法的IMP那么就会给我们一次补救的机会,调用resolveInstanceMethod方法。
  • 第二次调用的路径是 main->_CF_forwarding_prep_0->___forwarding___->methodSignatureForSelector->class_getInstanceMethod->lookUpImpOrForward->resolveInstanceMethod.其中_CF_forwarding_prep_0是系统库CoreFundation中的内容,它发起了第二次的resolveInstanceMethod,由于CoreFundation库并没有开源,接下来我们通过反汇编的形式,来探索之后的流程。

CoreFundation库的反汇编

准备工作

首先我们新建个iOS的工程,然后再main中打上断点,通过模拟器运行起来。 image.png 在断点的时候输入命令iamge list查看当前加载的库列表,找到CoreFoundation库的路径,然后将这个可执行文件使用Hopper反编译工具打开。

forwarding方法

我们在之前探索的过程中了解到,如果动态方法决议依旧没有处理方法的话,那么系统会调用__forwading__方法,而这个方法在CoreFoundation库中,接下来我们就来看看这个方法,在反编译的模式下,能看出点什么东西

image.png 可以看到,在伪代码的模式下,我们能够比较友好的来了解__forwarding__方法的功能。一部分一部分来看

int ____forwarding___(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5) {

// ……省略部分初始化相关……
loc_115fb4:
    var_150 = r12;
    var_140 = rdx;
    var_148 = r15;
    //**从这里可以知道,rbx实际上是实例对象,rax就是我们的目标类**
    rax = object_getClass(rbx);
    r15 = rax;
    r12 = class_getName(rax);
    //**r15 = rax,r15就是目标类,这里就是判断类中是否有forwardingTargetForSelector方法**
    //**如果没有则跳去loc_116064标记行,否则继续往下执行**
    //这个流程我们称之为,快速消息转发
    if (class_respondsToSelector(r15, @selector(forwardingTargetForSelector:)) == 0x0) goto loc_116064;

loc_115ff5:
    //**到这说明已经实现了forwardingTargetForSelector方法,直接调用它**
    rax = [rbx forwardingTargetForSelector:var_140];
    //**如果返回为0x0或者rax==rbx,即调用过后依旧没有找到方法的IMP**
    //**则直接跳去loc_116064标记行(与未实现方法跳转的地方一致)**
    if ((rax == 0x0) || (rax == rbx)) goto loc_116064;

loc_116012:
    //**如果返回不为0x0,即已经找到了方法的IMP,则跳去loc_11604a行**
    if (rax >= 0x0) goto loc_11604a;

在这段伪代码里,我们可以很明显的看到系统调用了一个叫做forwardingTargetForSelector的方法,也就是快速消息转发流程,如果成功匹配到IMP则去loc_11604a位置,否则去loc_116064位置,那么我们来看看他们分别都是什么

loc_11604a:
    //**var_148 = r15 也就是目标类**
    //**var_138是偏移量,这里应该是将找到的IMP插入到对应sel的缓存中**
    *(var_148 + var_138) = rax;
    r15 = 0x0;
    goto loc_11638e;

loc_11638e:
    //**用来检查进出函数时堆栈是否平衡和数据是否被修改**
    if (**___stack_chk_guard == **___stack_chk_guard) {
            rax = r15;
    }
    else {
            rax = __stack_chk_fail();
    }
    return rax;

这一段代码意思实际上就是,将找到的IMP匹配到SEL上,表示方法匹配成功。那么我们再看看loc_116064的代码

loc_116064:
    //**rbx就是实例对象,放到var_138暂存**
    var_138 = rbx;
    if (strncmp(r12, "_NSZombie_", 0xa) == 0x0) goto loc_1163cd;

loc_116087:
    //**r14 = 实例对象,r15 = 类对象**
    r14 = var_138;
    //**类对象是否响应methodSignatureForSelector方法**
    //**所以通过上面代码我们知道这是当forwardingTargetForSelector没匹配方法成功时调用**
    //**我们把这里成为慢速消息转发**
    if (class_respondsToSelector(r15, @selector(methodSignatureForSelector:)) == 0x0) goto loc_1163e3;

loc_1160a8:
    //**这里的rbx又等于SEL了**
    rbx = var_140;
    //**调用methodSignatureForSelector方法**
    rax = [r14 methodSignatureForSelector:rbx];
    //**返回值为0x0,则去loc_11645e位置**
    if (rax == 0x0) goto loc_11645e;
    
loc_1160c7:
    r15 = rax;
    rax = [rax _frameDescriptor];
    r12 = rax;
    if (((*(int16_t *)(*rax + 0x22) & 0xffff) >> 0x6 & 0x1) != r13) {
            rax = sel_getName(rbx);
            rcx = "";
            if ((*(int16_t *)(*r12 + 0x22) & 0xffff & 0x40) == 0x0) {
                    rcx = " not";
            }
            r8 = "";
            if (r13 == 0x0) {
                    r8 = " not";
            }
            _CFLog(0x4, @"*** NSForwarding: warning: method signature and compiler disagree on struct-return-edness of '%s'.  Signature thinks it does%s return a struct, and compiler thinks it does%s.", rax, rcx, r8, r9, var_150);
    }
    //**_forwardStackInvocation带下划线的基本都是系统内部函数**
    if (class_respondsToSelector(object_getClass(r14), @selector(_forwardStackInvocation:)) == 0x0) goto loc_1161fe;
    
loc_1161fe:
    var_150 = r12;
    r12 = r14;
    if (class_respondsToSelector(object_getClass(r14), @selector(forwardInvocation:)) == 0x0) goto loc_11642b;
    
loc_11622a:
    //**准备forwardInvocation方法需要的参数**
    rax = [NSInvocation _invocationWithMethodSignature:r15 frame:var_148];
    r13 = rax;
    //**调用forwardInvocation方法**
    [r12 forwardInvocation:rax];
    var_140 = 0x0;
    rbx = 0x0;
    r12 = var_150;
    goto loc_11626b;

loc_11642b:
    r14 = @selector(forwardInvocation:);
    ____forwarding___.cold.4(&var_130, r12);
    rcx = r14;
    _CFLog(0x4, @"*** NSForwarding: warning: object %p of class '%s' does not implement methodSignatureForSelector: -- trouble ahead", var_138, rcx, r8, r9, var_150);
    goto loc_116457;

这块就是当我们的快速消息转发流程依旧没有匹配到相应的IMP后,系统再给的一次机会,即慢速消息转发流程,而methodSignatureForSelector通常与forwardInvocation方法一起使用,组成完整的慢速消息转发流程。继续看如果慢速消息转发还没成功匹配,那么接下来系统会怎么做

loc_116457:
    rbx = var_140;
    goto loc_11645e;
    
loc_11645e:
    rax = sel_getName(rbx);
    r14 = rax;
    rax = sel_getUid(rax);
    if (rax != rbx) {
            rcx = r14;
            r8 = rax;
            _CFLog(0x4, @"*** NSForwarding: warning: selector (%p) for message '%s' does not match selector known to Objective C runtime (%p)-- abort", var_140, rcx, r8, r9, var_150);
    }
    if (class_respondsToSelector(object_getClass(var_138), @selector(doesNotRecognizeSelector:)) == 0x0) goto loc_1164d1;

loc_1164b8:
    (*_objc_msgSend)(var_138, @selector(doesNotRecognizeSelector:));
    asm { ud2 };
    rax = loc_1164d1(rdi, rsi, rdx, rcx, r8, r9);
    return rax;

这里很明显,就是调用doesNotRecognizeSelector方法,报错没有找到对应的方法,然后进入崩溃了。到这里我们已经整体的梳理了下快速消息转发慢速消息转发的的流程,但是还是没有解决我们一开始的问题:为什么会调用两次resolveInstanceMethod方法。我们再来看看第二次调用resolveInstanceMethod方法时的堆栈信息

image.png 我们可以看到,在调用methodSignatureForSelector后,调用了NSObjectmethodSignatureForSelector方法,那么我们去源码中看看这个方法的实现

image.png 苹果的注释告诉我们,这个方法的实现实际上是在CoreFundation库中,那么继续去反编译中查看该方法

void * +[NSObject instanceMethodSignatureForSelector:](void * self, void * _cmd, void * arg2) {
    rdx = arg2;
    rdi = self;
    //**如果调用方self != nil 并且___methodDescriptionForSelector返回不为空**
    //**则调用默认实现,否则返回空**
    if ((rdx != 0x0) && (___methodDescriptionForSelector(rdi, rdx) != 0x0)) {
            rax = [NSMethodSignature signatureWithObjCTypes:rdx];
    }
    else {
            rax = 0x0;
    }
    return rax;
}

这里在返回系统返回默认值之前,有个判断___methodDescriptionForSelector方法,而我们在堆栈中也看到了这个方法,那就继续看看他

int ___methodDescriptionForSelector(int arg0, int arg1) {
 //  ……省略……
loc_7c5d4:
    r15 = *(rax + r13 * 0x8);
    rdi = r12;
    r14 = r12;
    r12 = rax;
    rax = class_isMetaClass(rdi);
    rax = protocol_getMethodDescription(r15, rbx, 0x1, rax == 0x0 ? 0x1 : 0x0);
    if (rax != 0x0) goto loc_7c64e;

loc_7c602:
    rax = class_isMetaClass(r14);
    rax = protocol_getMethodDescription(*(r12 + r13 * 0x8), rbx, 0x0, rax == 0x0 ? 0x1 : 0x0);
    r12 = r14;
    if (rax != 0x0) goto loc_7c65c;
// ……中间省略……
loc_7c673:
    rax = class_getSuperclass(r12);
    r12 = rax;
    rbx = var_50;
    if (rax != 0x0) goto loc_7c5b4;

loc_7c68b:
    rax = class_getInstanceMethod(var_40, rbx);
    if (rax != 0x0) {
            rax = method_getDescription(rax);
            r15 = *rax;
            rbx = *(rax + 0x8);
    }
    else {
            rbx = 0x0;
            r15 = 0x0;
    }
    goto loc_7c6b2;
// ……省略……
}

其他的东西我们就不去研究了,但是我们在其中看到了一个我们很熟悉的方法class_getInstanceMethod没错,就是我们的慢速方法查找流程中的关键一环,而后会调用lookUpImpOrForward最后再次进入resolveInstanceMethod方法。打印两次的谜底在这里就揭开了。

反汇编总结

通过反汇编系统的CoreFundation库,我们大致的了解到了系统在动态消息决议之后做了什么

  • 快速消息转发流程 首先调用forwardingTargetForSelector方法,再给我们一次机会,快速的指定是否有其他的对象能够处理其他的方法。
  • 慢速消息转发流程 慢速消息转发流程由两个方法组成methodSignatureForSelectorforwardInvocation。这是系统给我们的最后一次机会,来动态的添加方法的实现。
  • 异常报错 当最后两次机会还没有抓住的话,系统就会进行异常报错,调用doesNotRecognizeSelector方法,最后崩溃。

快速消息转发流程

快速消息转发就是通过调用forwardingTargetForSelector方法,让我们能够快速的指定一个其他的对象,如果它实现了目标方法,那么也会成功的调用。我们举个例子🌰

@interface DMWorker : NSObject
- (void)sayBye;
@end

@implementation DMWorker
- (void)sayBye {
    NSLog(@"%s",__func__);
}
@end

@interface DMPerson : NSObject
- (void)sayBye;
@end

@implementation DMPerson
//快速消息转发
- (id)forwardingTargetForSelector:(SEL)aSelector
{
    NSLog(@"%s",__func__);
    if (aSelector == @selector(sayBye)) {
        return [DMWorker alloc];
    }
    return [super forwardingTargetForSelector:aSelector];
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        DMPerson *p = [[DMPerson alloc] init];
        [p sayBye];
    }
    return 0;
}

非常的简单,在DMWoker类中实现sayBye方法,在DMPerson快速方法查找中指定DMWorker响应sayBye方法,然后我们看看打印。

image.png DMPerson调用sayBye方法,DMWorker响应,很完美。但是这种方式调用的方法名和参数必须完全一样,比较死板,接下来就看看我们的慢速消息转发吧

慢速消息转发流程

相比快速转发流程慢速转发流程给与我们的自由度相对来说会大很多,我们继续来看个🌰

@interface DMPerson : NSObject
- (void)sayBye;
- (void)eat:(SEL)selector;
@end

@implementation DMPerson

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    NSLog(@"%s",__func__);
    return [super resolveInstanceMethod:sel];
}
//快速消息转发
- (id)forwardingTargetForSelector:(SEL)aSelector
{
    NSLog(@"%s",__func__);
    return [super forwardingTargetForSelector:aSelector];
}
//**慢速消息转发**
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    NSLog(@"%s",__func__);
    if (aSelector == @selector(sayBye)) {
        NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@::"];
        return signature;
    }
    return [super methodSignatureForSelector:aSelector];
}
//**这个方法里面是空实现也不会崩溃,但是没有转发的功能**
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    NSLog(@"%s",__func__);
    SEL originSel = anInvocation.selector;
    if (originSel == @selector(sayBye)) {
        anInvocation.target = self;
        anInvocation.selector = @selector(eat:);
        //**0位置是targe 1位置是sel,所以自定义的参数传递只能从2开始**
        [anInvocation setArgument:&originSel atIndex:2];
        [anInvocation invoke];
    }
}

- (void)eat:(SEL)selector{
    NSLog(@"%s: %@",__func__,NSStringFromSelector(selector));
}
@end


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        DMPerson *p = [[DMPerson alloc] init];
        [p sayBye];
    }
    return 0;
}

看看日志输出

image.png 成功的通过sayBye方法,调用了eat方法的实现。

当我们在methodSignatureForSelector中指定响应的NSMethodSignature后,那么方法的调用就不会崩溃,而在forwardInvocation方法中,是指定具体的响应方法。在forwardInvocation中我们可以将自己想要的任何方法、任何参数都给定制化。

总结

系统在通过快速方法查找慢速方法查找确定我们需要调用的方法没有实现以后,一共给我们三次机会去纠正这个错误

  • 动态方法决议 resolveInstanceMethod
  • 快速消息转发 forwardingTargetForSelector
  • 慢速消息转发 methodSignatureForSelector + forwardInvocation 我们可以通过下面这个图来梳理一下他们的关系

动态方法决议&消息转发流程图.png