010-消息转发

目前遗留的问题

  1. copy和strong修饰符的区别(objc_setProperty和内存平移, objc_getProperty都在什么情况下会调用)
  2. alloc的objc_alloc, objc_opt_class和objc_opt_isKindOfClass的符号查找
  3. 为什么第一次加载的时候firstSubclass=nil, 在执行或者调用了LGTeacher之后, 就会有firstSubclass=LGTeacher的赋值.

上一篇章遗留问题

    1. 动态方法决议走完之后, 源码中查询不到后续的流程了 , 是否就已经完毕了?
    1. 关于Class方法, 看源码逻辑先发送resolveClassMethod消息, 根据lookUpImpOrNilTryCache判断是否发送resolveInstanceMethod消息. 但是实际上是不会走resolveInstanceMethod方法的.

实际上测试了之后resolveInstanceMethod方法并不是不走了, 而是元类调用了resolveInstanceMethod找到了根元类, resolveInstanceMethod在NSObject里面实现了, 所以使用分类重写并打印resolveInstanceMethod. 是会出现打印的两次.

如果是实例方法找不到, 在本类重写了resolveInstanceMethod方法, 是不需要找到NSObject里面的. 因为在元类里面直接找得到, 不会在向根元类查询.

消息转发

事实上第一个问题的答案就是消息转发. 接下里我们新建一个项目:

@interface Person : NSObject
- (void)func1;
/**
 * 未实现
 */
- (void)instanceF;
+ (void)classF;
@end

@implementation Person
- (void)func1 {
    NSLog(@"%s", __func__);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSLog(@"%s %@", __func__, NSStringFromSelector(sel));
    return [super resolveInstanceMethod:sel];
}

@end

int main(int argc, char * argv[]) {
    Person *p = [Person alloc];
//未实现调用
    [p instanceF];
    return 0;
}
01.png

下面走了两次方法动态决议之后崩溃了. 这里我们看不出什么, 打个断点试一试:
整个堆栈信息如下:


02.png

可以看到第一次和第二次的堆栈调用:

  • _objc_msgSend_uncached->resolveMethod_locked->resolveInstanceMethod触发了+[Person resolveInstanceMethod:]

第二次是

  • CoreFoundation框架的__forwarding_prep_0___->___forwarding___->-[NSObject(NSObject) methodSignatureForSelector:]->__methodDescriptionForSelector->libobjc下面的class_getInstanceMethod->resolveMethod_locked->resolveInstanceMethod触发了+[Person resolveInstanceMethod:]

也就是说, 在我们平常开发中, 找不到方法实现的时候通常是会调用两次动态决议, 才会产生报错, 那么为什么会是这样, 我也不知道. 既然第二次是CF框架, 那么我们就看看CF里面有没有prep0看看是否能找到, 网址

我们把CF下载下来使用VSCode打开搜索, 发现不管是forwarding_prep_0_, prep, forwarding, methodSignatureForSelector通通搜不到. 所以可能是这部分没有开源.

反汇编CoreFoundation

找CoreFoundation

03.png

找到路径, 在文件夹中command+shift+G输入路径, 得到下图:


04.png

取出CoreFoundation可执行文件.

Hopper 反汇编CoreFoundation

hopper根据下图选择上面的可执行文件, 一路确定:


05.png

得到如下页面, 终于看到了我们想要的:


06.png

然后双击就可以进入对应的方法中:
int ____forwarding___(int arg0, int arg1) {
    rsi = arg1;
    rdi = arg0;
    r15 = rdi;
    var_30 = *___stack_chk_guard;
    rcx = COND_BYTE_SET(NE);
    if (rsi != 0x0) {
            r12 = _objc_msgSend_stret;
    }
    else {
            r12 = _objc_msgSend;
    }
    rax = rcx;
    rbx = *(r15 + rax * 0x8);
    rcx = *(r15 + rax * 0x8 + 0x8);
    var_140 = rcx;
    r13 = rax * 0x8;
    if ((rbx & 0x1) == 0x0) goto loc_649bb;

loc_6498b:
    rcx = *_objc_debug_taggedpointer_obfuscator;
    rcx = rcx ^ rbx;
    rax = rcx >> 0x1 & 0x7;
    if (rax == 0x7) {
            rcx = rcx >> 0x4;
            rax = (rcx & 0xff) + 0x8;
    }
    if (rax == 0x0) goto loc_64d48;

loc_649bb:
    var_148 = r13;
    var_138 = r12;
    var_158 = rsi;
    rax = object_getClass(rbx);
    r12 = rax;
    r13 = class_getName(rax);
    r14 = @selector(forwardingTargetForSelector:);
    if (class_respondsToSelector(r12, r14) == 0x0) goto loc_64a67;

loc_649fc:
    rdi = rbx;
    rax = _objc_msgSend(rdi, r14);
    if ((rax == 0x0) || (rax == rbx)) goto loc_64a67;

loc_64a19:
    r12 = var_138;
    r13 = var_148;
    if ((rax & 0x1) == 0x0) goto loc_64a5b;

loc_64a2b:
    rdx = *_objc_debug_taggedpointer_obfuscator;
    rdx = rdx ^ rax;
    rcx = rdx >> 0x1 & 0x7;
    if (rcx == 0x7) {
            rcx = (rdx >> 0x4 & 0xff) + 0x8;
    }
    if (rcx == 0x0) goto loc_64d45;

loc_64a5b:
    *(0x0 + r13) = rax;
    r15 = 0x0;
    goto loc_64d82;

loc_64d82:
    if (*___stack_chk_guard == var_30) {
            rax = r15;
    }
    else {
            rax = __stack_chk_fail();
    }
    return rax;

loc_64d45:
    rbx = rax;
    goto loc_64d48;

loc_64d48:
    if ((*(int8_t *)__$e48aedf37b9edb179d065231b52a648b & 0x10) != 0x0) goto loc_64ed1;

loc_64d55:
    rax = _getAtomTarget(rbx);
    r14 = rax;
    *(r15 + r13) = rax;
    ___invoking___(r12, r15);
    if (*r15 == r14) {
            *r15 = rbx;
    }
    goto loc_64d82;

loc_64ed1:
    ____forwarding___.cold.4();
    rax = *(rdi + 0x8);
    return rax;

loc_64a67:
    var_138 = rbx;
    if (strncmp(r13, "_NSZombie_", 0xa) == 0x0) goto loc_64dc1;

loc_64a8a:
    rbx = @selector(methodSignatureForSelector:);
    r14 = var_138;
    var_148 = r15;
    if (class_respondsToSelector(r12, rbx) == 0x0) goto loc_64dd7;

loc_64ab2:
    rax = _objc_msgSend(r14, rbx);
    rbx = var_158;
    if (rax == 0x0) goto loc_64e3c;

loc_64ad5:
    r12 = rax;
    rax = [rax _frameDescriptor];
    r13 = rax;
    if (((*(int16_t *)(*rax + 0x22) & 0xffff) >> 0x6 & 0x1) != rbx) {
            rax = sel_getName(var_140);
            rdx = " not";
            r8 = "";
            rcx = r8;
            if ((*(int16_t *)(*r13 + 0x22) & 0xffff & 0x40) == 0x0) {
                    rcx = rdx;
            }
            if (rbx == 0x0) {
                    r8 = rdx;
            }
            rdx = rax;
            _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.", rdx, rcx, r8, r9, stack[2003]);
    }
    var_150 = r13;
    if (class_respondsToSelector(object_getClass(r14), @selector(_forwardStackInvocation:)) == 0x0) goto loc_64c19;

loc_64b6c:
    if (*____forwarding___.onceToken != 0xffffffffffffffff) {
            dispatch_once(____forwarding___.onceToken, ^ { /* block implemented at ______forwarding____block_invoke */ });
    }
    r15 = [NSInvocation requiredStackSizeForSignature:r12];
    rsi = *____forwarding___.invClassSize;
    rsp = rsp - ___chkstk_darwin();
    r13 = rsp;
    __bzero(r13, rsi);
    rbx = rsp - ___chkstk_darwin();
    objc_constructInstance(*____forwarding___.invClass, r13);
    var_140 = r15;
    [r13 _initWithMethodSignature:r12 frame:var_148 buffer:rbx size:r15];
    [var_138 _forwardStackInvocation:r13];
    r14 = 0x1;
    goto loc_64c76;

loc_64c76:
    if (*(int8_t *)(r13 + 0x34) != 0x0) {
            rax = *var_150;
            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 = [r12 methodReturnType];
    rbx = rax;
    rax = *(int8_t *)rax;
    if ((rax != 0x76) && (((rax != 0x56) || (*(int8_t *)(rbx + 0x1) != 0x76)))) {
            r15 = *(r13 + 0x10);
            if (r14 != 0x0) {
                    r15 = [[NSData dataWithBytes:r15 length:var_140] bytes];
                    [r13 release];
                    rax = *(int8_t *)rbx;
            }
            if (rax == 0x44) {
                    asm{ fld        tword [r15] };
            }
    }
    else {
            r15 = ____forwarding___.placeholder;
            if (r14 != 0x0) {
                    [r13 release];
            }
    }
    goto loc_64d82;

loc_64c19:
    r15 = @selector(forwardInvocation:);
    if (class_respondsToSelector(object_getClass(r14), r15) == 0x0) goto loc_64ec2;

loc_64c3b:
    rax = [NSInvocation _invocationWithMethodSignature:r12 frame:var_148];
    r13 = rax;
    _objc_msgSend(0x0, r15);
    var_140 = 0x0;
    r14 = 0x0;
    goto loc_64c76;

loc_64ec2:
    rdi = var_130;
    ____forwarding___.cold.3(rdi, r14);
    goto loc_64ed1;

loc_64e3c:
    rax = sel_getName(var_140);
    r14 = rax;
    rax = sel_getUid(rax);
    if (rax != var_140) {
            r8 = rax;
            _CFLog(0x4, @"*** NSForwarding: warning: selector (%p) for message '%s' does not match selector known to Objective C runtime (%p)-- abort", var_140, r14, r8, r9, stack[2003]);
    }
    rbx = @selector(doesNotRecognizeSelector:);
    if (class_respondsToSelector(object_getClass(var_138), rbx) == 0x0) {
            ____forwarding___.cold.2(var_138);
    }
    rax = _objc_msgSend(var_138, rbx);
    asm{ ud2 };
    return rax;

loc_64dd7:
    rbx = class_getSuperclass(r12);
    r14 = object_getClassName(r14);
    if (rbx == 0x0) {
            _CFLog(0x4, @"*** NSForwarding: warning: object %p of class '%s' does not implement methodSignatureForSelector: -- did you forget to declare the superclass of '%s'?", var_138, r14, object_getClassName(var_138), r9, stack[2003]);
    }
    else {
            _CFLog(0x4, @"*** NSForwarding: warning: object %p of class '%s' does not implement methodSignatureForSelector: -- trouble ahead", var_138, r14, r8, r9, stack[2003]);
    }
    goto loc_64e3c;

loc_64dc1:
    ____forwarding___.cold.1(var_138, r13, var_140, rcx, r8);
    goto loc_64dd7;
}

反汇编的代码很长, 直接看的话很懵逼, 我们一步一步挑重点还原一下, 细节不一定准确, 只是大致流程.

void ____forwarding___(Class cls, SEL sel) {
    id receiver;
//loc_649bb
    if (class_respondsToSelector(cls, @selector(forwardingTargetForSelector:))) {
        receiver = objc_msgSend(cls, @selector(forwardingTargetForSelector:));
    }
//loc_649fc
    if (!receiver) {
        objc_msgSend(receiver, sel);
    } else {
//loc_64a8a
        if (class_respondsToSelector(cls, @selector(methodSignatureForSelector:))) {
            //loc_64ab2:
            objc_msgSend(cls, @selector(methodSignatureForSelector:));
            //loc_64ad5:
            if (class_respondsToSelector(cls, @selector(_forwardStackInvocation:))) { // 一般不实现, 先不管
                //loc_64b6c:
                objc_msgSend(cls, @selector(_forwardStackInvocation:));
                
            } else {
                //loc_64c19:
                if (class_respondsToSelector(cls, @selector(forwardInvocation:))) {
                    rax  = NSInvocation *
                    _objc_msgSend(0x0, @selector(forwardInvocation:));
                  //相当于执行了这个_objc_msgSend(rax, sel)
                  ret
                }
            }
        } else {
            goto loc_64e3c;
        }
    }
    
loc_64e3c    
if (class_respondsToSelector(cls, @selector(doesNotRecognizeSelector:))) {
        objc_msgSend(cls, @selector(doesNotRecognizeSelector:));
    };
}

大概就是这么个流程, 不过在流程里我们看到了我们今天要探索的:

  • 快速转发forwardingTargetForSelector
  • 慢速转发methodSignatureForSelector-forwardInvocation

快速转发

07.png

我们实现了方法forwardingTargetForSelector之后, 发现确实如prep_0一样,
其实我们可以这样去验证反汇编, 就是我们打断点. 如下:
08.png

反汇编的寄存器的值, 在这里每个阶段都可以读取到, 然后再去配合着去读就会舒服很多.

我们用Proxy类实现instanceF方法, 然后在快速转发中返回会发生什么呢?

09.png

不会崩溃了, 而且第二的转发也不会走了, 问题解决了. 所以我们在第一层只需要返回一个id消息接受者, 接受者就会自动调用objc_msdSend然后继续进行消息处理.

从反汇编看到doesNotRecognizeSelector所以我有点好奇, 我打印了一下如下:

10.png

也就是说, 写一个NSObject一个分类, 重写doesNotRecognizeSelector可以看到一次打印.
我们下面继续实现一下慢速转发的两个方法.

慢速转发

methodSignatureForSelectorforwardInvocation是结合着使用的, 单独使用任何一个都没有用.

  • methodSignatureForSelector返回方法签名
  • forwardInvocation拿到方法签名, 设置接收者去实现, 或者不处理.
    先看一下处理的效果:
    11.png

    神奇吧, 没有处理也没有崩溃.
    但是看到图11的打印顺序会不会很奇怪, 第一次只走到了methodSignatureForSelector就没有继续往下走了, 然后继续调用了resolveInstanceMethod然后_forwardStackInvocation, 最后forwardInvocation.

也就是说, 消息转发的流程另外一种情况如下

  • 1.实现了methodSignatureForSelector, 并且返回了对应的方法签名.
  • 2.实现了forwardInvocation方法,
    在第二次进行转发的消息是_forwardStackInvocation(如果_forwardStackInvocation没有实现, 则需要forwardInvocation去响应), 不然就报错.

我们在回到汇编中看一下:


16.png

大致就是这么个流程, 虽然画图技术拙劣但是已经尽量的表达清楚了.

在慢速转发中, 主要的点就是

  • methodSignatureForSelectorforwardInvocation一起搭配使用.
  • methodSignatureForSelector有正确返回值的准确情况下, 会走_forwardStackInvocationforwardInvocation只需要实现方法即不会崩溃.
  • methodSignatureForSelector没有返回值的情况下, forwardInvocation是无效的. 怎么处理都没有用.

继续看下面两张图:


18.png
19.png

图18和19, 只有一个区别就是一个是强引用了一个Proxy的实例对象, 另外一个是局部变量, 在这个地方如果使用局部变量会被释放掉, 报出对象的野指针错误, 所以在这个地方调用的话, 要注意一点.

Thread 1: "-[Person instanceF]: unrecognized selector sent to instance 0x600000988160"

类的过程

类方法如下, 探究过程一样

+ (BOOL)resolveClassMethod:(SEL)sel {
    NSLog(@"%s %@", __func__, NSStringFromSelector(sel));
    return [super resolveClassMethod:sel];
}

+ (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"%s %@", __func__, NSStringFromSelector(aSelector));
    return [super methodSignatureForSelector:aSelector];
}

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

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

第二次流程

苹果显然是设计的消息流程, 给了两次转发的机会, 但是第二次的转发在原文最上面看到了这么一个调用methodDescriptionForSelector, 我们并没有在汇编中看到, 那么它是在哪里的呢?

看图2是怎么从methodSignatureForSelector__methodDescriptionForSelector. 做出如下改动:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSLog(@"%s %@", __func__, NSStringFromSelector(aSelector));
    return nil;//返回值变为nil
}
20.png

动态决议走了一次, 消息转发到慢速阶段后面就没有了.

接下来我们看NSObject的定义:


21.png

这可咋办, 啥也没写, 我们现在只知道整个二次转发流程和[super methodSignatureForSelector]肯定是有关系的.... 不要着急, 找一份objc的源码看看有没有什么新收获.源码

22.png

23.png

看见没, 这些都是CF的实际调用, 扩展给了objc使用. 现在感觉快看到希望了, 回到反汇编继续找CF.

void * -[NSObject methodSignatureForSelector:](void * self, void * _cmd, void * arg2) {
    rdx = arg2;
    rdi = self;
    if ((rdx != 0x0) && (___methodDescriptionForSelector([rdi class], rdx) != 0x0)) {
            rax = [NSMethodSignature signatureWithObjCTypes:rdx];
    }
    else {
            rax = 0x0;
    }
    return rax;
}

int ___methodDescriptionForSelector(int arg0, int arg1) {
//省略部分代码
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;
}



function class_getInstanceMethod {
    rax = _class_getInstanceMethod(rdi, rsi);
    return rax;
}

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

    // This deliberately avoids +initialize because it historically did so.

    // This implementation is a bit weird because it's the only place that 
    // wants a Method instead of an IMP.

#warning fixme build and search caches
        
    // Search method lists, try method resolver, etc.
    lookUpImpOrForward(nil, sel, cls, LOOKUP_RESOLVER);

#warning fixme build and search caches

    return _class_getMethod(cls, sel);
}

[NSObjec methodSignatureForSelector]>___methodDescriptionForSelector->>class_getInstanceMethod->objc源码中->lookUpImpOrForward回到了消息慢速查找流程.
整个闭环, 结束.

总结

消息转发的所有流程, 不管是快速转发还是慢速转发, 都跟__forwarding_prep_0___有关, 第一次转发和第二次转发每一个环节变量的控制走与不走, 都需要自己进行大量的测试, 因为没有这些测试, 只是看这些枯燥的代码是记不住这些繁琐的流程的.

消息过程非常重要, 可以说每一个语言的消息流程设计都是核心中的核心, 有兴趣的小伙伴探索一下吧.

1

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

推荐阅读更多精彩内容