第十二节—objc_msgSend(四)消息转发

本文为L_Ares个人写作,以任何形式转载请表明原文出处。

本节开始说明在动态决议依然没有找到selimp的话,系统还有没有留给我们机会去防止报错,或者说程序的crash。

其实在看到lookUpImpOrForward这个慢速查找流程的除了动态决议还有一个方法done

图片.png

但是你会发现走到这里的都是都是imp找到了以后的情况。

所以我们可以进去看一看。

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

你可以找到bool objcMsgLogEnabled = false;,所以if里面是一定会实现的了。

那么看if里面,这里会记录一个缓存的状态和日志,那么再进入logMessageSend看看。

图1.png

会发现默认的情况下好像是不产生日志的,那么其实可以找一下怎么让它产生日志,所以就搜索一下objcMsgLogEnabled还有哪里有赋值。

找到如下函数

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

也就是说,如果我给这个函数一个true它就可以打印日志,日志可以存储到上面图1说的/tmp/msgSends-xxx的文件里面。

那么到这里,我们新建一个mac项目,来看一下,当我不给一个方法做实现的时候,log里面会记录怎样的事情。

于是我新建了一个

图2.png

并添加一个继承于NSObject的类JDPerson,只给一个方法,但是不给实现。

然后把instrumentObjcMessageSendsmain.m做一个外部声明,这样就可以扩充它,进行自定义的调试。

于是main.m中就存在如下代码 :

#import 
#import 

@interface JDPerson : NSObject

- (void)myTest;

@end

@implementation JDPerson

@end

extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        JDPerson *person = [[JDPerson alloc] init];
        instrumentObjcMessageSends(true);
        [person myTest];
        instrumentObjcMessageSends(false);
        NSLog(@"Hello, World!");
    }
    return 0;
}

我在方法调用之前打开日志,并在方法调用之后关闭日志,然后去找到日志,看看方法的调用能打印出什么日志。

然后我们前往/tmp文件夹,找日志,日志的前缀一定是msgSends。图1代码有说。

图3.png
图4.png

找到后打开看一下。

图5.png

resolveInstanceMethod这个很熟悉,这是实力方法的动态决议,那么下面的forwardingTargetForSelectormethodSignatureForSelector是什么?

这个就是本节要说的,在动态决议没有办法解决imp找不到的情况以后,系统还会给机会,进行消息转发。

那么关于forwardingTargetForSelector其实commond + shift + 0可以打开官方的开发文档看一下。

图6.png

还有methodSignatureForSelector

图7.png

其实这两个就是两个消息转发的方式,也是本节的重点,在看图5的log里面就可以知道,系统在动态决议之后,先进行了forwardingTargetForSelector,然后再进行了methodSignatureForSelector,也就是说在forwardingTargetForSelector没有找到imp以后才会去找methodSignatureForSelector。于是我们常称

  • forwardingTargetForSelector : 消息的快速转发

  • methodSignatureForSelector : 消息的慢速转发

来区别它们。

一、forwardingTargetForSelector

那么我们可以在main.m中按照下面这么去做。

#import 
#import 

@interface JDStudent : NSObject

//- (void)myTest;

@end

@implementation JDStudent

- (void)myTest
{
    NSLog(@"转发到了JDStudent的canDoThis身上");
}

@end

@interface JDPerson : NSObject

- (void)myTest;

@end

@implementation JDPerson

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if (aSelector == @selector(myTest)) {
        return [JDStudent alloc];
    }
    
    return [super forwardingTargetForSelector:aSelector];
}

@end


extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        JDPerson *person = [[JDPerson alloc] init];
        [person myTest];
        NSLog(@"Hello, World!");
    }
    return 0;
}

结果图 :

图8.png

可以看到,JDStudent也是继承于NSObject,甚至都没有声明自己有myTest方法,只是在实现的里面出现过这个方法,经过快速消息转发,一样可以找到sel对应的imp

所以快速消息转发会将消息交给一个你指定的,非nil、非self对象来处理。

二、methodSignatureForSelector

当上面的消息快速转发流程还是不能处理方法的imp找不到的问题的时候,我们就会来到methodSignatureForSelector

在官方文档中,你可以看到

图9.png

返回的是一个信号,那么我们去看一下NSMethodSignature又是什么,怎么获得?

图10.png

一个继承于NSObject的类。

图11.png

这里就要看一下这个初始化对象的方法了。

+ (NSMethodSignature *)signatureWithObjCTypes:(const char *)types;

也就是说,我们要先获取一个方法imp的信号,获取的方式是把方法imp的类型编译告诉他。

那么拿到这个NSMethodSignature信号之后,我们需要转发的sel就变成了NSMethodSignature信号,然后就变成了转发这个信号。

也就是还要调用一步转发信号,看看谁要处理这个信号 :

- (void)forwardInvocation:(NSInvocation *)anInvocation

那么我们修改最开始的代码如下 :

#import 
#import 

@interface JDStudent : NSObject

//- (void)myTest;

@end

@implementation JDStudent

- (void)myTest
{
    NSLog(@"转发到了JDStudent的canDoThis身上");
}

@end

@interface JDPerson : NSObject

- (void)myTest;

@end

@implementation JDPerson

- (id)forwardingTargetForSelector:(SEL)aSelector
{
//    if (aSelector == @selector(myTest)) {
//        return [JDStudent alloc];
//    }
    
    return [super forwardingTargetForSelector:aSelector];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    
    if (aSelector == @selector(myTest)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    NSLog(@"%s",__func__);
    
    SEL aSelector = [anInvocation selector];
    
    JDStudent *student = [[JDStudent alloc] init];
    
    if ([student respondsToSelector:aSelector]) {
        
        [anInvocation invokeWithTarget:student];
        
    }else{
        [super forwardInvocation:anInvocation];
    }
    
}

@end


//extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        JDPerson *person = [[JDPerson alloc] init];
        [person myTest];
        NSLog(@"Hello, World!");
    }
    return 0;
}

执行结果 :

图12.png

另外,其实只要实现了forwardInvocation,里面什么都不写,也是没有问题的。

因为慢速消息转发和快速消息转发不一样,慢速消息转发将没有impsel变成了一个信号,就放在那里,谁可以执行,或者你指定谁去执行都是可以的,反正就是不会走到unrecognized selector sent to instance这个报错上面去。

三、解决上一节的问题

还记得上一节——动态决议中还有一个问题没有解决吧。

在动态决议中,resolveInstanceMethodresolveClassMethod如果只是实现,但是并没有执行class_addMethod,也即没有给sel匹配imp的情况下,会执行两次。其实这两个问题的原因是一样的,都是sel匹配不到imp,所以看一个就可以。

这里我们来解决,我在JDPerson的实现中添加resolveInstanceMethod,但是添加class_addMethod

#import 
#import 

@interface JDStudent : NSObject

//- (void)myTest;

@end

@implementation JDStudent

- (void)myTest
{
    NSLog(@"转发到了JDStudent的canDoThis身上");
}

@end

@interface JDPerson : NSObject

- (void)myTest;

@end

@implementation JDPerson

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

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    
    NSLog(@"%s",__func__);
//    if (aSelector == @selector(myTest)) {
//        return [JDStudent alloc];
//    }
    
    return [super forwardingTargetForSelector:aSelector];
}

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

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

    SEL aSelector = [anInvocation selector];

    JDStudent *student = [[JDStudent alloc] init];

    if ([student respondsToSelector:aSelector]) {

        [anInvocation invokeWithTarget:student];

    }else{
        [super forwardInvocation:anInvocation];
    }

}

@end


//extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        JDPerson *person = [[JDPerson alloc] init];
        [person myTest];
        NSLog(@"Hello, World!");
    }
    return 0;
}

然后我们看NSLog的打印 :

图13.png

这张图13可以说是动态决议及消息转发的一个流程了。

很明显,

(1). 如果我们没有在动态决议中给sel添加imp,那么就会进入快速消息转发
(2). 如果快速消息转发还是没有添加selimp连接,那么就会进入慢速消息转发
(3). 慢速消息转发首先生成了方法信号。
(4). 然后系统有去查询了一次动态方法解析是否执行了class_addMethod
(5). 最后,才会转发方法信号。

四、objc_msgSend消息发送流程的总结

  • 快速方法查找流程,objc_msgSend发送了对象sel,也可以添加上参数。然后会根据对象上的isa找到cls,然后到cls的缓存cache中的buckets中进行快速方法查找(循环遍历),看有没有bucketsel和我们传入的sel一致。

    • 有的话会返回对应的imp
    • 如果缓存中没有找到sel,那么就会通过CheckMiss或者JumpMiss进入方法的慢速查找流程。
  • 慢速方法查找流程,也就是进入方法列表进行查询。慢速查找流程会沿着继承链利用二分法查找算法进行查找selimp,当cls的方法列表中没有imp

    • 如果继承链上可以找到sel对应的imp,则返回imp,退出循环。
    • 继承链上的父类、根类的缓存中都没有imp,即当发现返回nil的情况下,退出循环,进入动态决议。
  • 动态决议,这是系统为我们准备的解决crash的方法,分为实例动态决议resolveInstanceMethod和类动态决议resolveClassMethod,我们可以通过class_addMethod手动的为sel添加一个imp,并且再次进入慢速方法查找流程

    • 如果执行了class_addMethod,则sel可以在找到imp,并通过慢速方法查找流程返回imp
    • 如果没有执行class_addMethod,则会进入消息转发流程。
  • 消息转发,进入消息转发后,sel会通过methodSignatureForSelector变成方法信号,然后会被放在那里,就像一个失物招领,等着人来认领。

    • methodSignatureForSelectorsel变成信号后,系统还会再一次的进行动态决议,查看是否实现了class_addMethod
      • 如果实现了,那么就会把selimp连接起来,直接执行imp
      • 如果没有实现,则会向下进行。
    • 如果你通过forwardInvocationNSInvocation找到一个target,也即是有人认领了方法,那么就查看target有没有selimp
      • 如果targetsel对应的imp,则会正常返回imp
      • 但是,如果target也没有这个信号中的selimp,那么就会进入报错流程unrecognized selector sent to instance
    • 如果forwardInvocation中什么都不写,则方法信号就一直漂流四处等待认领,但是不会进入unrecognized selector sent to instance报错流程。

你可能感兴趣的:(第十二节—objc_msgSend(四)消息转发)