Objective-C 消息转发

一、概述

上一篇文章分析到了 方法动态决议,当方法动态决议找不到imp的时候就来到了消息转发,这篇文章将详细分析消息转发。

1.1 instrumentObjcMessageSends 分析思路

在方法动态决议找不到imp的时候上篇文章定义了一个函数instrumentObjcMessageSends打印了后续方法的调用日志:

extern void instrumentObjcMessageSends(BOOL flag);

那么这个函数怎么来的呢?
在找到imp的时候imp会调用log_and_fill_cache插入缓存:

static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
    if (slowpath(objcMsgLogEnabled && implementer)) {
        bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                      cls->nameForLogging(),
                                      implementer->nameForLogging(), 
                                      sel);
        if (!cacheIt) return;
    }
#endif
    cls->cache.insert(sel, imp, receiver);
}

当在osx下会调用logMessageSend打印日志:

image.png

条件是objcMsgLogEnabled && implementer,走到这里implementer肯定是有的,那么就需要查看objcMsgLogEnabled(默认false)是怎么赋值的。搜索找到了如下代码:

extern bool objcMsgLogEnabled;
bool objcMsgLogEnabled = false;

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);
   //赋值
    objcMsgLogEnabled = enable;
}

所以,也只有instrumentObjcMessageSends能决定objcMsgLogEnabled的取值,所以把这个方法暴露出去,也就能监听方法调用生成日志了。

就得到了方法动态决议找不到imp的后续流程了:

forwardingTargetForSelector:
methodSignatureForSelector:
resolveInstanceMethod:
doesNotRecognizeSelector:

二、消息快速转发 forwardingTargetForSelector

搜索官方文档有如下定义:

- (id)forwardingTargetForSelector:(SEL)aSelector;
+ (id)forwardingTargetForSelector:(SEL)aSelector;

这个方法让对象有机会重定向发送给它的未知消息,比常规消息转发快一个数量级。分别对应类方法和实例方法。
HPObject调用没有实现的方法instanceMethod,在进行缓存查找,慢速查找,方法动态决议后会进入消息快速转发forwardingTargetForSelector方法中,HPSubObject如果实现了instanceMethod方法,则可以直接交给HPSubObject处理:

//自己解决不了,找一个相同类型的方法
- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
    if (aSelector == @selector(instanceMethod)) {
        return [HPSubObject alloc];
    }
    return [super forwardingTargetForSelector:aSelector];
}

这个时候调用HPObject调用instanceMethod最终会调用到HPSubObjectinstanceMethod方法。

forwardingTargetForSelector可以对于自己没有实现的方法直接重定向,没有方法动态决议那么麻烦。如果需要对参数或返回值进行操作,那么这个方法就不适用了。

  • forwardingTargetForSelector有机会重定向发送给它的未知消息。
  • 比慢速转发快一个数量级。
  • 不能对参数和返回值进行操作。
  • 返回值为备用消息接收者。
  • 返回值不能为nil或者自己,否则进入下一个流程。
  • 在非根类中实现,没有自己的特定返回值则需要调用super

三、消息慢速转发 methodSignatureForSelector

如果上面的快速转发流程仍然没有解决问题,则会进入消息慢速转发,methodSignatureForSelector定义如下:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;

这个方法对于给定的sel返回方法的签名对象NSMethodSignature。在官方文档中已经说明这个方法伴随着forwardInvocation一起使用:

- (void)forwardInvocation:(NSInvocation *)anInvocation;
+ (void)forwardInvocation:(NSInvocation *)anInvocation;

简单打印下日志:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
    return [super methodSignatureForSelector:aSelector];
}

发现确实能够进入该方法的调用,但是仍然没有解决问题,上面已经说过了真正解决问题是靠forwardInvocation,他们两个必须成对出现。修改实现如下:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {  
}

运行后仍然报错是因为没有返回签名信息。继续修改:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
    if (aSelector == @selector(instanceMethod)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"%s - %@ - %@",__func__,anInvocation.target,NSStringFromSelector(anInvocation.selector));
}

签名信息可以任意给,不一定需要与给定的sel进行匹配,提供可用的方法签名就可以。
输出:

-[HPObject methodSignatureForSelector:] - instanceMethod
-[HPObject forwardInvocation:] - <HPObject: 0x1014430c0> - instanceMethod

虽然没有对这个消息进行操作,但是接收消息已经不报错了。所有消息对于系统来说都是事务,可处理可不处理。anInvocation会被保存,当需要的时候会进行处理。

如果要处理呢?可以修改如下:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
    if (aSelector == @selector(instanceMethod)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"%s - %@ - %@",__func__,anInvocation.target,NSStringFromSelector(anInvocation.selector));
//    anInvocation.target = [HPSubObject alloc];
//    [anInvocation invoke];//执行
    HPSubObject *subObject = [HPSubObject alloc];
    if ([self respondsToSelector:anInvocation.selector]) { //自己能够响应
        [anInvocation invoke];
    } else if ([subObject respondsToSelector:anInvocation.selector]){
        [anInvocation invokeWithTarget:subObject];
    } else {
        //可以在这里上报错误等。
        NSLog(@"%s - %@",__func__,NSStringFromSelector(anInvocation.selector));
        [super forwardInvocation:anInvocation];
    }
}

根据自身业务和需求灵活处理。

慢速转发相对于快速转发给了很大的灵活性。如果提炼出来在NSObject分类中实现,对于OC方法找不到的崩溃都能避免掉。这个只是假象的消失,造成了很多资源的浪费。进入到这里必然经历了很多流程。

doesNotRecognizeSelector
当慢速转发后仍然没有解决问题的时候会进入doesNotRecognizeSelector

- (void)doesNotRecognizeSelector:(SEL)aSelector;
+ (void)doesNotRecognizeSelector:(SEL)aSelector;

那如果重写这个方法能不能处理错误呢?

If you override this method, you must call super or raise an invalidArgumentException exception at the end of your implementation. In other words, this method must not return normally; it must always result in an exception being thrown.

这个方法只是让异常可控,拿到错误信息而已。并不能处理错误。

  • methodSignatureForSelector对于给定的sel返回签名对象NSMethodSignature
  • methodSignatureForSelector必须与forwardInvocation成对出现一起使用。
  • methodSignatureForSelector要处理未知消息必须给定签名信息,签名信息只要是可用的即可。
  • forwardInvocation可以不实现具体内容,如果要处理需要判断后invoke
  • 慢速转发相比快速转发提供了很大的灵活性,但是会造成资源浪费,能进入这里证明经历了很多流程。
  • doesNotRecognizeSelector并不能解决问题,这个方法只是能拿到错误信息,让异常可控。

四、反汇编分析消息转发

上面的快速和慢速消息转发都是基于官方文档进行的分析,如果没有资料或者说不熟悉这块应该怎么入手呢?
在调用crash后直接bt

image.png

可以看到CoreFoundation框架直接调用了___forwarding_____forwarding_prep_0___后执行了doesNotRecognizeSelector
直接打开opensource搜索CoreFoundation,很遗憾没有搜到。在网址后面拼CF直接打开CoreFoundation源码地址(这是一个隐藏的路径)。
image.png

下载最新的版本打开搜索___forwarding_____forwarding_prep_0___并没有相关内容。(苹果没有开源这一部分)。

4.1 反汇编分析

4.1.1 CoreFoundation 提取

既然苹果没有开源,那就只剩一个办法了,反汇编分析CoreFoundation动态库。新版本的macOS对应的CoreFoundation苹果对齐进行了隐藏。那么还有两个方法获取CoreFoundation动态库:

  1. 直接新建一个iOS工程模拟器运行起来然后image list找到CoreFoundation的路径拷贝一份:

    image.png

  2. 使用越狱手机拷贝系统的动态库:

scp -r -P 12345  root@localhost:/System/Library/Caches/com.apple.dyld  ./Framework

由于系统的动态库是一个库,所以直接拷贝整个dyld_shared_cache_arm64(大小2G多)。

4.1.2 Hopper 分析 CoreFoundation 库

使用Hopper或者IDA打开CoreFoundation/dyld_shared_cache_arm64动态库分析反汇编代码。

如果是分析dyld_shared_cache_arm64,打开dyld_shared_cache_arm64的时候搜索选择CoreFoundation即可:

image.png

当然也可以选择使用dsc_extractor进行拆分这个dsc文件。

./dsc_extractor path/to/dyld_shared_cache_arm64  outputdir

这样就在拆分的库中能找到CoreFoundation了。

4.1.3 forwarding_prep_0伪代码分析

Hopper分析完毕后直接搜索forwarding_prep_0查看反汇编伪代码:

int ___forwarding_prep_0___(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5) {
    //……
    rax = ____forwarding___(&stack[0], 0x0);
    if (rax != 0x0) {
            rax = *rax;
    }
    else {
          //arg0,arg1
            rax = objc_msgSend(stack[0], stack[8]);
    }
    return rax;
}
  • 可以看到内部是对___forwarding___的调用。
  • ____forwarding___返回值不存在的时候调用的是objc_msgSend参数是arg0
    arg1

4.1.4 __forwarding__伪代码分析

点击进去查看___forwarding___的实现:

int ____forwarding___(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5) {
    r9 = arg5;
    r8 = arg4;
    rcx = arg3;
    r13 = arg1;
    r15 = arg0;
    rax = COND_BYTE_SET(NE);
    if (arg1 != 0x0) {
            r12 = *_objc_msgSend_stret;
    }
    else {
            r12 = *_objc_msgSend;
    }
    rbx = *(r15 + rax * 0x8);
    rsi = *(r15 + rax * 0x8 + 0x8);
    var_140 = rax * 0x8;
    if (rbx >= 0x0) goto loc_115af7;

loc_115ac0:
    //target pointer处理
    rax = *_objc_debug_taggedpointer_obfuscator;
    rax = *rax;
    rcx = (rax ^ rbx) >> 0x3c & 0x7;
    rax = ((rax ^ rbx) >> 0x34 & 0xff) + 0x8;
    if (rcx != 0x7) {
            rax = rcx;
    }
    if (rax == 0x0) goto loc_115ea6;

loc_115af7:
    var_150 = r12;
    var_138 = rsi;
    var_148 = r15;
    rax = object_getClass(rbx);
    r15 = rax;
    r12 = class_getName(rax);
    //是否能响应 forwardingTargetForSelector,不能响应跳转 loc_115bab 否则继续执行  也就是forwardingTargetForSelector方法返回nil或者自身
    if (class_respondsToSelector(r15, @selector(forwardingTargetForSelector:)) == 0x0) goto loc_115bab;

loc_115b38:
    //rax返回值
    rax = [rbx forwardingTargetForSelector:var_138];
    //返回值是否存在,返回值是否等于自己 是则跳转 loc_115bab
    if ((rax == 0x0) || (rax == rbx)) goto loc_115bab;

loc_115b55:
    if (rax >= 0x0) goto loc_115b91;

loc_115b5a:
    rcx = *_objc_debug_taggedpointer_obfuscator;
    rcx = *rcx;
    rdx = (rcx ^ rax) >> 0x3c & 0x7;
    rcx = ((rcx ^ rax) >> 0x34 & 0xff) + 0x8;
    if (rdx != 0x7) {
            rcx = rdx;
    }
    if (rcx == 0x0) goto loc_115e95;

loc_115b91:
    *(var_148 + var_140) = rax;
    r15 = 0x0;
    goto loc_115ef1;

loc_115ef1:
    if (**___stack_chk_guard == **___stack_chk_guard) {
            rax = r15;
    }
    else {
            rax = __stack_chk_fail();
    }
    //返回 forwardingTargetForSelector 为消息的接收者
    return rax;

loc_115e95:
    rbx = rax;
    r15 = var_148;
    r12 = var_150;
    goto loc_115ea6;

loc_115ea6:
    if (dyld_program_sdk_at_least(0x7e30901ffffffff) != 0x0) goto loc_116040;

loc_115ebd:
    r14 = _getAtomTarget(rbx);
    *(r15 + var_140) = r14;
    ___invoking___(r12, r15, r15, 0x400, 0x0, r9, var_150, var_148, var_140, var_138, var_130, stack[-304], stack[-296], stack[-288], stack[-280], stack[-272], stack[-264], stack[-256], stack[-248], stack[-240]);
    if (*r15 == r14) {
            *r15 = rbx;
    }
    goto loc_115ef1;

loc_116040:
    ____forwarding___.cold.1();
    rax = objc_opt_class(@class(NSInvocation));
    *____forwarding___.invClass = rax;
    rax = class_getInstanceSize(rax);
    *____forwarding___.invClassSize = rax;
    return rax;

loc_115bab:
    var_140 = rbx;
    //是否僵尸对象
    if (strncmp(r12, "_NSZombie_", 0xa) == 0x0) goto loc_115f30;

loc_115bce:
    r14 = var_140; 
    //是否能够响应 methodSignatureForSelector
    if (class_respondsToSelector(r15, @selector(methodSignatureForSelector:)) == 0x0) goto loc_115f46;

loc_115bef:
    rbx = var_138;
    //调用
    rax = [r14 methodSignatureForSelector:rbx];
    if (rax == 0x0) goto loc_115fc1;

loc_115c0e:
    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_115d61;

loc_115c9a:
    if (*____forwarding___.onceToken != 0xffffffffffffffff) {
            dispatch_once(____forwarding___.onceToken, ^ {/* block implemented at ______forwarding____block_invoke */ } });
    }
    [NSInvocation requiredStackSizeForSignature:r15];
    var_138 = r15;
    rdx = *____forwarding___.invClassSize;
    r13 = &var_150 - (rdx + 0xf & 0xfffffffffffffff0);
    memset(r13, 0x0, rdx);
    objc_constructInstance(*____forwarding___.invClass, r13);
    var_150 = rax;
    r15 = var_138;
    [r13 _initWithMethodSignature:var_138 frame:var_148 buffer:&stack[-8] - (0xf + rax & 0xfffffffffffffff0) size:rax];
    [var_140 _forwardStackInvocation:r13];
    rbx = 0x1;
    goto loc_115dce;

loc_115dce:
    if (*(int8_t *)(r13 + 0x34) != 0x0) {
            rax = *r12;
            if (*(int8_t *)(rax + 0x22) < 0x0) {
                    rcx = *(int32_t *)(rax + 0x1c);
                    rdx = *(int8_t *)(rax + 0x20) & 0xff;
                    memmove(*(rdx + var_148 + rcx), *(rdx + rcx + *(r13 + 0x8)), *(int32_t *)(*rax + 0x10));
            }
    }
    rax = [r15 methodReturnType];
    r14 = rax;
    rax = *(int8_t *)rax;
    if ((rax != 0x76) && (((rax != 0x56) || (*(int8_t *)(r14 + 0x1) != 0x76)))) {
            r15 = *(r13 + 0x10);
            if (rbx != 0x0) {
                    r15 = [[NSData dataWithBytes:r15 length:var_150] bytes];
                    [r13 release];
                    rax = *(int8_t *)r14;
            }
            if (rax == 0x44) {
                    asm { fld        tword [r15] };
            }
    }
    else {
            r15 = ____forwarding___.placeholder;
            if (rbx != 0x0) {
                    r15 = ____forwarding___.placeholder;
                    [r13 release];
            }
    }
    goto loc_115ef1;

loc_115d61:
    var_138 = r12;
    r12 = r14;
    //forwardInvocation的判断,如果没有实现直接跳转loc_115f8e
    if (class_respondsToSelector(object_getClass(r14), @selector(forwardInvocation:)) == 0x0) goto loc_115f8e;

loc_115d8d:
    rax = [NSInvocation _invocationWithMethodSignature:r15 frame:var_148];
    r13 = rax;
    [r12 forwardInvocation:rax];
    var_150 = 0x0;
    rbx = 0x0;
    r12 = var_138;
    goto loc_115dce;

loc_115f8e:
    //错误日志
    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_140, rcx, r8, r9, var_150);
    goto loc_115fba;

loc_115fba:
    rbx = var_138;
    goto loc_115fc1;

loc_115fc1:
    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_138, rcx, r8, r9, var_150);
    }
    if (class_respondsToSelector(object_getClass(var_140), @selector(doesNotRecognizeSelector:)) == 0x0) goto loc_116034;

loc_11601b:
    [var_140 doesNotRecognizeSelector:rdx];
    asm { ud2 };
    rax = loc_116034(rdi, rsi, rdx, rcx, r8, r9);
    return rax;

loc_116034:
    ____forwarding___.cold.3(var_140);
    goto loc_116040;

loc_115f46:
    rbx = class_getSuperclass(r15);
    r14 = object_getClassName(r14);
    if (rbx == 0x0) {
            rax = object_getClassName(var_140);
            rcx = r14;
            r8 = rax;
            _CFLog(0x4, @"*** NSForwarding: warning: object %p of class '%s' does not implement methodSignatureForSelector: -- did you forget to declare the superclass of '%s'?", var_140, rcx, r8, r9, var_150);
    }
    else {
            rcx = r14;
            _CFLog(0x4, @"*** NSForwarding: warning: object %p of class '%s' does not implement methodSignatureForSelector: -- trouble ahead", var_140, rcx, r8, r9, var_150);
    }
    goto loc_115fba;

loc_115f30:
    r14 = @selector(forwardingTargetForSelector:);
    ____forwarding___.cold.2(var_140, r12, var_138, rcx, r8);
    goto loc_115f46;
}

可以看到汇编伪代码的调用流程与看到的API调用流程差不多。

4.1.5 __forwarding__伪代码还原

还原主要逻辑伪代码如下:

#include <stdio.h>

@interface NSInvocation(additions)

+ (unsigned long long)requiredStackSizeForSignature:(NSMethodSignature *)signature;

-(id)_initWithMethodSignature:(id)arg1 frame:(void*)arg2 buffer:(void*)arg3 size:(unsigned long long)arg4;

+(id)_invocationWithMethodSignature:(id)arg1 frame:(void*)arg2;

@end


@interface NSObject(additions)

- (void)_forwardStackInvocation:(NSInvocation *)invocation;

@end


void forwardingTargetForSelector(Class cls, SEL sel, const char * className, id obj);
void methodSignatureForSelector(Class cls, id obj, SEL sel);
void doesNotRecognizeSelector(id obj, SEL sel);
void _forwardStackInvocation(id obj,NSMethodSignature *signature);
void forwardInvocation(id obj,NSMethodSignature *signature);

int ____forwarding___(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5) {
    SEL sel = NULL;
    id obj;
    Class cls = object_getClass(obj);
    const char * className = class_getName(cls);
    forwardingTargetForSelector(cls,sel,className,obj);
    return 0;
}

void forwardingTargetForSelector(Class cls, SEL sel, const char * className, id obj) {
    //是否能响应 forwardingTargetForSelector,不能响应跳转 loc_115bab 否则继续执行  也就是forwardingTargetForSelector方法返回nil或者自身
    if (class_respondsToSelector(cls, @selector(forwardingTargetForSelector:))) {
        id obj = [cls forwardingTargetForSelector:sel];
        if ((obj == nil) || (obj == cls)) {
            methodSignatureForSelector(cls,obj,sel);
        } else if (obj >= 0x0) {
            //返回 forwardingTargetForSelector 备用消息接收者
//            return obj;
        } else {
            //taggedpointer 处理
            //返回NSInvocation size数据
        }
    } else {
        //是否僵尸对象
        if (strncmp(className, "_NSZombie_", 0xa)) {
            methodSignatureForSelector(cls,obj,sel);
        } else {
            SEL currentSel = @selector(forwardingTargetForSelector:);
            doesNotRecognizeSelector(obj,currentSel);
        }
    }
}


void methodSignatureForSelector(Class cls, id obj, SEL sel) {
    if (class_respondsToSelector(cls, @selector(methodSignatureForSelector:))) {
        NSMethodSignature *signature = [obj methodSignatureForSelector:sel];
        if (signature) {
            _forwardStackInvocation(obj,signature);
        } else {
            doesNotRecognizeSelector(obj,sel);
        }
    } else {
        doesNotRecognizeSelector(obj,sel);
    }
}

void _forwardStackInvocation(id obj,NSMethodSignature *signature) {
    //是否能够响应_forwardStackInvocation
    if (class_respondsToSelector(object_getClass(obj), @selector(_forwardStackInvocation:))) {
        //执行dispatch_once相关逻辑
        [NSInvocation requiredStackSizeForSignature:signature];
        void *bytes;
//        objc_constructInstance([NSInvocation class], bytes);
        NSInvocation *invocation = [invocation _initWithMethodSignature:signature frame:NULL buffer:NULL size:bytes];
        [obj _forwardStackInvocation:invocation];
        const char * type = [signature methodReturnType];
        //返回signature
    } else {
        forwardInvocation(obj,signature);
    }
}

void forwardInvocation(id obj,NSMethodSignature *signature) {
    //forwardInvocation的判断,如果没有实现直接跳转loc_115f8e
    if (class_respondsToSelector(object_getClass(obj), @selector(forwardInvocation:))) {
        NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:signature frame:NULL];
        [obj forwardInvocation:invocation];
        const char * type = [signature methodReturnType];
        //返回signature
    } else {
        SEL sel = @selector(forwardInvocation:);
        doesNotRecognizeSelector(obj,sel);
    }
}

void doesNotRecognizeSelector(id obj, SEL sel) {
    if (class_respondsToSelector(object_getClass(obj), @selector(doesNotRecognizeSelector:))) {
        [obj doesNotRecognizeSelector:sel];
        /*
         ____forwarding___.cold.1();
         rax = objc_opt_class(@class(NSInvocation));
         *____forwarding___.invClass = rax;
         rax = class_getInstanceSize(rax);
         *____forwarding___.invClassSize = rax;
         return rax;
         */
    } else {
        /*
         ____forwarding___.cold.1();
         rax = objc_opt_class(@class(NSInvocation));
         *____forwarding___.invClass = rax;
         rax = class_getInstanceSize(rax);
         *____forwarding___.invClassSize = rax;
         return rax;
         */
    }
}

为了方便分析我这里class-dumpCoreFoundation头文件。手机端使用cycript进入SpringBoard应用,然后classdumpdyld导出CoreFoudation的头文件,最后拷贝到电脑端,具体操作如下:

cycript -p SpringBoard
@import net.limneos.classdumpdyld;
classdumpdyld.dumpBundle([NSBundle > bundleWithIdentifier:@"com.apple.CoreFoudation"]);
//输出导出头文件路径
@"Wrote all headers to /tmp/CoreFoundation"
//拷贝到电脑的相应目录
scp -r  -P 12345  root@localhost:/tmp/CoreFoundation/  ./CoreFoundation_Headers/

详情可以看我之前的文章class-dump
⚠️在导出的NSObject头文件中并没有发现_forwardStackInvocation方法。目前并不清楚这个方法是在哪里定义的。

伪代码流程图如下:


消息转发反汇编流程

反汇编流程与根据API分析的流程差不多。

  • forwardingTargetForSelector快速转发会对返回值会进行判断,如果是返回的自身或者nil直接进入下一流程(慢速转发)。
  • 如果返回taggedpointer有单独的处理。
  • methodSignatureForSelector慢速转发会先判断有没有实现_forwardStackInvocation(私有方法)。实现_forwardStackInvocation后不会再进入forwardInvocation流程,相当于_forwardStackInvocation是一个私有的前置条件。
  • methodSignatureForSelector如果没有返回签名信息不会继续进行下面的流程。
  • forwardInvocation没有实现就直接走到doesNotRecognizeSelector流程了。

4.2 流程分析

上篇文章分析resolveInstanceMethod在消息转发后还会调用一次resolveInstanceMethod(在日志文件中看到是在doesNotRecognizeSelector之前,methodSignatureForSelector之后)。那么实现对应的方法做下验证:

HPObject resolveInstanceMethod: HPObject-0x100008290-instanceMethod
-[HPObject forwardingTargetForSelector:] - instanceMethod
-[HPObject methodSignatureForSelector:] - instanceMethod
HPObject resolveInstanceMethod: HPObject-0x100008290-instanceMethod
-[HPObject doesNotRecognizeSelector:] - instanceMethod

证实是在methodSignatureForSelector之后,doesNotRecognizeSelector之前有一次进行了方法动态决议。那么为什么要这么处理呢?因为消息转发的过程中可能已经加入了对应的sel-imp,所以再给一次机会进行方法动态决议。这次决议后不会再进行消息转发。

但是在反汇编分析中并没有明确的再次进行动态方法决议的逻辑。

4.2.1 反汇编以及源码探究

那么在第二次调用resolveInstanceMethod前打断点查看下堆栈信息
macOS堆栈如下:

* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 5.1
    frame #0: 0x0000000100300f53 libobjc.A.dylib`resolveMethod_locked(inst=0x0000000000000000, sel="instanceMethod", cls=HPObject, behavior=0) at objc-runtime-new.mm:6339:13
    frame #1: 0x00000001002ffbd5 libobjc.A.dylib`lookUpImpOrForward(inst=0x0000000000000000, sel="instanceMethod", cls=HPObject, behavior=0) at objc-runtime-new.mm:6601:16
    frame #2: 0x00000001002d6df9 libobjc.A.dylib`class_getInstanceMethod(cls=HPObject, sel="instanceMethod") at objc-runtime-new.mm:6210:5
  * frame #3: 0x00007fff2e33fc68 CoreFoundation`__methodDescriptionForSelector + 282
    frame #4: 0x00007fff2e35b57c CoreFoundation`-[NSObject(NSObject) methodSignatureForSelector:] + 38
    frame #5: 0x0000000100003a21 HPObjcTest`-[HPObject methodSignatureForSelector:](self=0x0000000100706a30, _cmd="methodSignatureForSelector:", aSelector="instanceMethod") at HPObject.m:29:12 [opt]
    frame #6: 0x00007fff2e327fc0 CoreFoundation`___forwarding___ + 408
    frame #7: 0x00007fff2e327d98 CoreFoundation`__forwarding_prep_0___ + 120
    frame #8: 0x0000000100003c79 HPObjcTest`main + 153
    frame #9: 0x00007fff683fecc9 libdyld.dylib`start + 1
    frame #10: 0x00007fff683fecc9 libdyld.dylib`start + 1

可以看到methodSignatureForSelector调用后进入了__methodDescriptionForSelector随后调用了class_getInstanceMethod。查看汇编确实在__methodDescriptionForSelector中调用了class_getInstanceMethod

image.png

那么系统是如何从methodSignatureForSelector调用到__methodDescriptionForSelector的?
当前的methodSignatureForSelector的实现是:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
    return [super methodSignatureForSelector:aSelector];
}

如果改为返回nil呢?

HPObject resolveInstanceMethod: HPObject-0x100008288-instanceMethod
-[HPObject forwardingTargetForSelector:] - instanceMethod
-[HPObject methodSignatureForSelector:] - instanceMethod
-[HPObject doesNotRecognizeSelector:] - instanceMethod

这个时候发现没有第二次调用了,那也就是说核心逻辑在[super methodSignatureForSelector:aSelector]的实现中。
查看源码:

// Replaced by CF (returns an NSMethodSignature)
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    _objc_fatal("+[NSObject methodSignatureForSelector:] "
                "not available without CoreFoundation");
}

// Replaced by CF (returns an NSMethodSignature)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    _objc_fatal("-[NSObject methodSignatureForSelector:] "
                "not available without CoreFoundation");
}

注释说的已经很明显了实现在CoreFoundation中,直接搜索methodSignatureForSelector的反汇编实现:

/* @class NSObject */
-(void *)methodSignatureForSelector:(void *)arg2 {
    rdx = arg2;
    if ((rdx != 0x0) && (___methodDescriptionForSelector(objc_opt_class(), rdx) != 0x0)) {
            rax = [NSMethodSignature signatureWithObjCTypes:rdx];
    }
    else {
            rax = 0x0;
    }
    return rax;
}
  • sel不为nil的时候会调用___methodDescriptionForSelector。这样就串联起来了。

class_getInstanceMethod的实现如下:

Method class_getInstanceMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;
    lookUpImpOrForward(nil, sel, cls, LOOKUP_RESOLVER);
    return _class_getMethod(cls, sel);
}

4.2.2 断点调试验证

既然上面已经清楚了resolveInstanceMethod第二次调用是methodSignatureForSelector之后调用的,那么不妨打个符号断点跟踪下methodSignatureForSelector:

image.png

显然只需要关心调用的函数以及跳转逻辑。

跟进去__methodDescriptionForSelector

image.png

继续进入class_getInstanceMethod

image.png

继续跳转:


image.png

这样就定位到了lookUpImpOrForward,继续进入lookUpImpOrForward,最终会定位到resolveInstanceMethod

image.png

这样通过断点也从methodSignatureForSelector定位到了resolveInstanceMethod

结论:

  • 实例方法 - methodSignatureForSelector-> ___methodDescriptionForSelector -> class_getInstanceMethod-> lookUpImpOrForward->resolveMethod_locked-> resolveInstanceMethod
  • 类方法 + methodSignatureForSelector -> ___methodDescriptionForSelector(传递的是元类) -> class_getInstanceMethod- lookUpImpOrForward->resolveMethod_locked-> resolveClassMethod

⚠️总结:

  1. 在methodSignatureForSelector内部调用了class_getInstanceMethod进行lookUpImpOrForward随后进入方法动态决议。这也就是class_getInstanceMethod调用第二次的来源入口。
  2. methodSignatureForSelector后第二次调用class_getInstanceMethod是为了再给一次进行消息查找和动态决议流程,因为消息转发流程过程中有可能实现了对应的sel-imp

动态方法决议以及消息转发整个流程如下:

方法动态决议&消息转发

五、消息发送查找总结

前面已经通过objc_msgSend分析整个消息缓存、查找、决议、转发整个流程。

  • 通过CacheLookup进行消息快速查找
    • 整个cache查找过程相当于是insert过程的逆过程,找到imp就解码跳转,否则进入慢速查找流程。
  • 通过lookUpImpOrForward进行消息慢速查找
    • 慢速查找涉及到递归查找,查找过程分为二分查找/循环查找。
    • 找到imp直接跳转,否则查找父类缓存。父类缓存依然找不到则在父类方法列表中查找,直到找到nil。查找到父类方法/缓存方法直接插入自己的缓存中。
  • imp找不到的时候进行方法动态决议
    • 当快速和慢速消息查找都没有找到imp的时候就进入了方法动态决议流程,在这个流程中主要是添加imp后再次进行快速慢速消息查找。
  • 之后进入本篇的消息转发流程,消息转发分为快速以及慢速。
    • 在动态方法决议没有返回imp的时候就进入到了消息转发阶段。
    • 快速消息转发提供一个备用消息接收者,返回值不能为nil与自身。这个过程不能修改参数和返回值。
    • 慢速消息转发需要提供消息签名,只要提供有效签名就可以解决消息发送错误问题。同时要实现forwardInvocation配合处理消息。
    • forwardInvocation配合处理消息,使target生效起作用。
    • 在慢速消息转发后系统会再进行一次慢速消息查找流程。这次不会再进行消息转发。
    • 消息转发仍然没有解决问题会进入doesNotRecognizeSelector,这个方法并不能处理错误,实现它仍然会报错。只是能拿到错误信息而已。

⚠️慢速消息转发后系统仍然给了一次机会进行 慢速消息查找!!!(并不仅仅是动态方法决议)。

整个流程如下:


objc_msgSend消息处理流程
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容