objc_msgsend(下)消息转发

122 阅读4分钟

引入

在学习本文之前我们应该了解

本文将继续沿用循循渐进的思想,来龙去脉搞清楚是本文的目的。由于CoreFoundation未完全开源,我们将引用一些反汇编工具。

准备工作

instrumentObjcMessageSends辅助

instrumentObjcMessageSends的引入是由于在源码阅读中观察到log_and_fill_cache也就是缓存插入insert流程中有信息输出判断如下:

#if SUPPORT_MESSAGE_LOGGING
    if (slowpath(objcMsgLogEnabled && implementer)) {
        bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                      cls->nameForLogging(),
                                      implementer->nameForLogging(), 
                                      sel);
        if (!cacheIt) return;
    }
#endif

bool*objcMsgLogEnabled = false;

static int objcMsgLogFD = -1;

bool logMessageSend(bool isClassMethod,

                    const char *objectsClass,

                   const char** *implementingClass,

                    **SEL** selector)

{
    char buf[ 1024 ];
    if (objcMsgLogFD == (-1))

    {
        snprintf (buf, sizeof(buf), "/tmp/msgSends-%d", (int) getpid ());

        objcMsgLogFD = secure_open (buf, O_WRONLY | O_CREAT, geteuid());

        if (objcMsgLogFD < 0) {
            objcMsgLogEnabled = false;
            objcMsgLogFD = -1;
            return true;
        }

    }
    snprintf(buf, sizeof(buf), "%c %s %s %s\n",

            isClassMethod ? '+' : '-',

            objectsClass,

            implementingClass,

            sel_getName(selector));
    objcMsgLogLock.lock();

    write (objcMsgLogFD, buf, strlen(buf));

    objcMsgLogLock.unlock();
    return false;
}

void instrumentObjcMessageSends(BOOL flag)
{
    bool enable = flag;
    if(objcMsgLogEnabled == enable)

        return;
    if (enable)

        _objc_flush_caches(Nil);

    if (objcMsgLogFD != -1)

        fsync (objcMsgLogFD);

    objcMsgLogEnabled = enable;

}

  • 为什么会注意到这个函数,因为logMessageSend的实现能输出sel
  • 这里有两个判断条件implementer以及objcMsgLogEnabledimplementer 这个是当前类肯定存在,所以只要找到如何控制objcMsgLogEnabledinstrumentObjcMessageSends控制objcMsgLogEnabled
  • instrumentObjcMessageSendsEXTERN修饰,这是对外暴露的。所有我们可以在外部扩展它。
  • 文件存放目录/tmp/msgSends

1.png

forwardingTargetForSelector 探究

最好的方式打开苹果文档查看api解释。快捷键common+shift+0;

  • 定义: 返回未被识别的消息应该首先被引导到的对象。意思就是消息你不能处理告诉我谁能处理就返回这个对象。就是找个背锅侠接住这个消息。

  • 声明

- (id)forwardingTargetForSelector:(SEL)aSelector;
  • 参数 aSelector 一个没有实现的方法的选择器。就是方法sel还没有找到imp的。

  • 返回值 未被识别的信息应首先被指向的对象

  • 方法说明补充

如果一个对象实现了(或继承了)这个方法,并且返回了一个非零(和非自)的结果,那么这个返回的对象就会被用作新的接收对象,并且消息调度会恢复到这个新对象。(显然,如果你从这个方法中返回self,代码就会陷入一个无限循环)。 如果你在一个非根类中实现这个方法,如果你的类对于给定的选择器没有什么可返回的,那么你应该返回调用super的实现的结果这个方法给了一个对象一个机会在昂贵得多的forwardInvocation:机制接管之前,重定向一个未知的消息发送给它。当你只是想把消息重定向到另一个对象时,这个方法很有用,而且可以比常规的转发快一个数量级。如果转发的目标是捕获 NSInvocation,或在转发过程中操纵参数或返回值,那么它就没有用。

大概意思就是:如果你在类里面实现了这个方法返回了一个可以接受消息的对象,那么消息将被新对象调用。 如果你实现了这个方法没什么可以返回的那么你就调用父类这个方法返回。这个方法给了你一个机会,而且他很快。这个就是消息快速转发方法

#import <Foundation/Foundation.h>
#import "LGPerson.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *person = [LGPerson alloc];
        [person sayHello];
    }
    return 0;
    
---------------------------------------------------    

#import "LGPerson.h"
#import "LGStudent.h"
@implementation LGPerson
-(id)forwardingTargetForSelector:(SEL)aSelector{
    return [LGStudent alloc];
}      

--------------------------------------------------

#import "LGStudent.h"
@implementation LGStudent
- (void)sayHello{
    NSLog(@"%s", **__func__** );
}

输出打印:2021-12-02 20:55:37.628130+0800-forwardingTargetForSelector分析[45426:282951] -[LGStudent sayHello]
}

methodSignatureForSelector 探究

  • 定义: 返回一个NSMethodSignature对象,该对象包含对由给定选择器识别的方法的描述。-- 就是方法签名;
  • 声明
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
  • 参数 aSelector 一个选择器,用于识别要返回实现地址的方法。当接收者是一个实例时,Selector应该标识一个实例方法;当接收者是一个类时,它应该标识一个类方法。

  • 返回值 未被识别的信息应首先被指向的对象。一个NSMethodSignature对象,包含了对aSelector所识别的方法的描述,如果找不到该方法,则为nil。

  • 方法说明补充 该方法用于协议的实现。该方法也用于必须创建NSInvocation对象的情况,例如在消息转发期间。如果你的对象维护了一个委托,或者能够处理它没有直接实现的消息,你应该覆盖这个方法以返回一个适当的方法签名。 -- 注意:``这个方法搭配forwardInvocation一起使用

forwardInvocation 探究

  • 定义: 被子类重载,以转发消息给其他对象 -
  • 声明
- (void)forwardInvocation:(NSInvocation *)anInvocation;
  • 参数 anInvocation 要转发的调用。

  • 方法说明补充
    当一个对象被发送一个它没有对应方法的消息时,运行时系统会给接收者一个机会,将该消息委托给另一个接收者。它通过创建一个代表该消息的NSInvocation对象并向接收者发送一个包含该NSInvocation对象作为参数的forwardInvocation:消息来委托该消息。然后,接收方的forwardInvocation:方法可以选择将消息转发给另一个对象。(如果该对象也不能响应该消息,它也将被给予一个转发的机会)。 forwardInvocation: 消息因此允许一个对象与其他对象建立关系,对于某些消息,这些对象将代表它行事。在某种意义上,转发对象能够 "继承 "它所转发的消息的对象的一些特性

  • 重要 为了响应你的对象本身不承认的方法,除了 forwardInvocation: 之外,你必须覆盖 methodSignatureForSelector:。转发消息的机制使用从 methodSignatureForSelector: 获得的信息来创建要被转发的 NSInvocation 对象。你的重写方法必须为给定的选择器提供一个适当的方法签名,可以通过预先制定一个或向另一个对象索取一个。

#import <Foundation/Foundation.h>
#import "LGPerson.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *person = [LGPerson alloc];
        [person sayInstanceMethod];
        NSLog(@"Hello, World!");
    }

    return 0;
    
---------------------------------------------------    

#import "LGStudent.h"

@implementation LGStudent

- (void)sayInstanceMethod{

    NSLog(@"%s", __func__ );

}

@end
--------------------------------------------------- 

#import "LGPerson.h"
#import "LGStudent.h"

@implementation LGPerson

- (NSMethodSignature *)methodSignatureForSelector:(**SEL**)aSelector{

    if (aSelector == @selector(sayInstanceMethod)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    }

    return  [super methodSignatureForSelector:aSelector];

}

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

    LGStudent * t = [LGStudent alloc];

    if ([t respondsToSelector: anInvocation.selector]) {
        [anInvocation invokeWithTarget:t];
    }else{
        NSLog(@"%s - %@", __func__ ,NSStringFromSelector(anInvocation.selector));
}
}

2021-12-03 20:08:55.603498+0800 [12170:72635] -[LGStudent sayInstanceMethod]

2021-12-03 20:08:55.603498+0800 [12170:72635] Hello, World!

堆栈分析流程

1.png

  • CoreFoundtion框架还没完全开源。

反汇编Hopper分析

7.png

动态方法决议走两次的原因

还在🤔

objc_msgsend(下)消息转发流程图

8.png

总结

  • 快速转发:通过forwardingTargetForSelector实现,如果此时有指定的对象去接收这个消息,就会走之指定对象的查找流程,如果返回是nil,进入慢速转发流程
  • 慢速转发:通过methodSignatureForSelectorforwardInvocation共同实现,如果methodSignatureForSelector返回值是nil,慢速查找流程结束,如果有返回值forwardInvocation的事务处理不处理都不会崩溃,sel会保存在Invocation里面随时等待你处理。