美文网首页
OC消息转发机制

OC消息转发机制

作者: 大橘猪猪侠 | 来源:发表于2020-09-24 16:06 被阅读0次

在上一篇文章中,我们探索了objc_msgSend慢速转发的流程;那么我们知道在进行慢速查找过程中,如果meth有值的话,就能获得imp,接着就会执行代码goto done;代码

done中的源码只有两行:
log_and_fill_cache(cls, imp, sel, inst, curClass); runtimeLock.unlock();

重点在第一行的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
    cache_fill(cls, sel, imp, receiver);
}

在源码中,当你满足SUPPORT_MESSAGE_LOGGING条件之后,他就会执行logMessageSend方法,它会打印执行的方法,下面附上源码:

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;
}

但是,这个方法它是进不来的,为什么呢?
因为objcMsgLogEnabled这个是默认关闭的,在logMessageSend函数下面的方法就是打开这个函数的开关,这个函数是void instrumentObjcMessageSends(BOOL flag),具体源码就不贴出来了,只要将这个方法打开,就能将执行的方法打印到一个文件中,下面我们来尝试一下:

首先创建一个工程,在工程中创建一个类,具体代码,其中sayHello方法只有声明,没有实现:

#import <Foundation/Foundation.h>
#import "Person.h"

extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        Person *person = [Person alloc];
        instrumentObjcMessageSends(YES);
        [person sayHello];
        instrumentObjcMessageSends(NO);
        NSLog(@"Hello, World!");
    }
    return 0;
}

接着我们先来看一下上面方法中出现的文件路径/tmp/msgSends/

16009289145594.png

没有执行程序时,是这样的;现在我们来执行一下上面的代码,执行之后,文件中多了一个msgSends-11018文件:

16009289866111.png

我们打开一下这个文件,下面截取一部分内容显示出来:


16009290557511.png

在里面的内容中,resolveInstanceMethod是进行动态方法决议,在进行动态方法决议之后,又执行了forwardingTargetForSelectormethodSignatureForSelector两个方法。

既然系统会调用这两个方法,那我们去看看这两个方法到底是什么:
首先我们以上面创建的项目在Person.m中实现一个- (id)forwardingTargetForSelector:(SEL)aSelector方法,具体实现代码:

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

执行程序之后,程序崩溃,打印信息为:

-[Person forwardingTargetForSelector:] - sayHello
-[Person sayHello]: unrecognized selector sent to instance 0x1007457e0

我们先来看一下官方的解释:Returns the object to which unrecognized messages should first be directed.

意思是:当消息者没有接收者时,返回它的第一接收者;也就是说,如果消息没有人接收,那就可以找一个人接收。

那么我们再修改一下代码:

在工程中继续创建一个Student类,类中声明和实现一下sayHello这个方法,接着在Person.m中修改代码:

- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
    return [Student alloc];
}

在执行程序之后,打印结果为(省略了前面的时间和工程名):

-[Person forwardingTargetForSelector:] - sayHello
-[Student sayHello]
Hello, World!

可以看到,程序可以正常执行,可以看到sayHello方法能够执行,执行的类为Student。类似帅锅一样的,这就是所谓的快速转发过程。

那既然有快速转发,那是否存在慢速转发呢?
下面我们来看一下methodSignatureForSelector这个方法的作用:

首先看一下官方解释:对那些没有转发和处理的方法,系统这边还会有一层处理,返回一个方法的签名,搭配一个方法的使用;进行方法签名时,不能返回为空。

下面我们继续通过代码来实现一下:
在Person.m中另外添加一个方法,首先先测试一下方法会不会执行:

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


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

执行结果,程序崩溃,执行了两次:

-[Person forwardingTargetForSelector:] - sayHello
-[Person methodSignatureForSelector:] - sayHello
-[Person sayHello]: unrecognized selector sent to instance 0x10044c0e0
(lldb) 

同样的,程序都会执行两个方法,下面根据官方的提示,来实现一下methodSignatureForSelector的正确过程:

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

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

这个方法签名在Type Encodings中有详细介绍。
执行结果:

-[Person forwardingTargetForSelector:] - sayHello
-[Person methodSignatureForSelector:] - sayHello
-[Person forwardInvocation:] - <NSInvocation: 0x102808e10>
Hello, World!
Program ended with exit code: 0

程序正确执行;首先快速转发流程来了,然后方法签名和forwardInvocation都来了,没有崩溃。

下面我们来看一下forwardInvocation这个方法,这个方法中有一个NSInvocation类,我们点进去看一下他的结构:

@interface NSInvocation : NSObject

+ (NSInvocation *)invocationWithMethodSignature:(NSMethodSignature *)sig;

@property (readonly, retain) NSMethodSignature *methodSignature;

- (void)retainArguments;
@property (readonly) BOOL argumentsRetained;

@property (nullable, assign) id target;
@property SEL selector;

可以看到他有很多属性,我们在forwardInvocation方法处打上断点,然后在控制它输出他的属性看一下:

16009314308429.png

可以看到里面属性的一些内容;由此可见,它是一个消息慢速转发过程,它类似于一个漂流瓶一样的,谁爱处理谁处理。
因此,我们可以自己设定谁来处理,例如将它的target进行修改,然后invoke保存:

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s - %@",__func__,anInvocation);
    anInvocation.target = [Student alloc];
    // anInvocation 保存 - 方法
    [anInvocation invoke];
}

他的执行结果,可以看到是Student进行输出sayHello:

-[Person forwardingTargetForSelector:] - sayHello
-[Person methodSignatureForSelector:] - sayHello
-[Person forwardInvocation:] - <NSInvocation: 0x10060d1e0>
-[Student sayHello]
Hello, World!
Program ended with exit code: 0

消息的慢速转发是一个很灵活的方法,它可以随意指定任何对象进行消息转发,并随时进行保存。

下面总结一下整个过程:
当慢速查找流程没有找到IMP,首先判断当前是否为类,是类就执行resolveClassMethod方法,不是类就执行resolveInstanceMethod,但是,resolveClassMethod方法最后还会走到resolveInstanceMethod处;
接着进行快速转发执行forwardingTargetForSelector,当快速转发执行成功,进行消息转发,当执行不成功,进行慢速转发methodSignatureForSelector,慢速转发返回签名forwardInvocation,最后进行消息转发。

以上所有流程皆来自上帝视角,那么如果没有上帝视角,那如何得到消息转发的流程呢?
例如我们将Person中的方法注释掉,在main函数中执行sayHello方法。

结果必然是崩溃的,那么我们通过堆栈信息打印一下看:


16009328233391.png

在样的内容看起来很繁琐,当我们去查看___forwarding___的汇编信息,可以得知它所存在的库在CoreFoundation中。

但是经过在库中搜索__forwarding_prep_0___,却找不到我们想要的内容。
那么只有通过反汇编去hopper。首先通过image list去打印库文件信息,找到CoreFoundation所在路径:/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation,将可执行文件拖入hopper。

拖入进去后的界面:


16009337594264.png

搜索__forwarding_prep_0___,通过点击伪代码来查看源码:

int ___forwarding_prep_0___(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5) {
    *(rsp + 0xa0) = zero_extend_64(xmm7);
    *(rsp + 0x90) = zero_extend_64(xmm6);
    *(rsp + 0x80) = zero_extend_64(xmm5);
    *(rsp + 0x70) = zero_extend_64(xmm4);
    *(rsp + 0x60) = zero_extend_64(xmm3);
    *(rsp + 0x50) = zero_extend_64(xmm2);
    *(rsp + 0x40) = zero_extend_64(xmm1);
    *(rsp + 0x30) = zero_extend_64(xmm0);
    stack[2021] = arg0;
    rax = ____forwarding___(rsp, 0x0);
    if (rax != 0x0) {
            rax = *rax;
    }
    else {
            rax = objc_msgSend(stack[2021], stack[2021]);
    }
    return rax;
}

在上面的伪代码中,点击____forwarding___方法,去查看它的源码:

16009339494827.png

在这个方法中,找到了一段代码,里面就有在上面探索的forwardingTargetForSelector方法。

if处成立,goto loc_64a67:

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

继续goto loc_64dc1

loc_64dc1:
    ____forwarding___.cold.1(var_138, r13, var_140, rcx, r8);
    goto loc_64dd7;
}
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;

方法没有实现,报错。

伪代码的流程,我就不详细介绍了,接着继续查看伪代码:

接着又找到了methodSignatureForSelector方法和forwardInvocation方法

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

接着又返回到loc_64dd7,这些伪代码同样的跟上面拥有上帝视角的流程差不太多。

这就是反汇编的整个消息转发流程。

相关文章

  • 关于Runtime 消息发送机制的延伸

    说到OC 不得不说一下OC 的消息转发机制;何为OC 的消息转发机制;其实就是这样的; Objc 在向一个对象发送...

  • OC消息机制,消息转发机制

    Runtime简称运行时,其中最主要的是消息机制 概述 C 与 OC 的不同 1.C 语言,函数的调用在编译的时候...

  • Runtime知识点整理1

    OC消息机制?消息转发机制流程?什么是Runtime?什么场景下使用? ==============巴拉巴拉......

  • oc消息转发机制

    一、消息转发机制 在OC中,调用一个对象的方法,实际上是给对象发了一条消息,在编译Objective-C函数调用的...

  • OC消息转发机制

    当一个对象收到它没实现的消息的时候,通常会发生如下的情况。 调用+(BOOL)resolveInstanceMet...

  • OC 消息转发机制

    首先了解几个概念:class 的定义 method的定义 消息转发本质:在运行时将方法地址(imp)和一个名字(s...

  • OC 消息转发机制。

    当调用一个 NSObject 对象不存在的方法时,并不会马上抛出异常,而是会经过多层转发,层层调用对象的-reso...

  • OC 消息转发机制

    首先我们看一下objc_msgSend它具体是如何发送消息: 首先根据receiver对象的isa指针获取它对应的...

  • OC消息转发机制

    消息转发的两大阶段 先征询接收者,所属的类,看其是否能够动态添加方法,以处理这个『未知的选择子』,这叫做动态方法解...

  • OC消息转发机制

    暂时先看这篇文章消息转发

网友评论

      本文标题:OC消息转发机制

      本文链接:https://www.haomeiwen.com/subject/zwttuktx.html