美文网首页
runtime-消息传递与转发机制

runtime-消息传递与转发机制

作者: sy随缘 | 来源:发表于2019-03-14 10:24 被阅读0次

    参考文章:
    继承自NSObject的不常用又很有用的函数【重点推荐】
    Objective-C Runtime 1小时入门教程【重点推荐】
    类的本质-类对象
    运行时消息传递与转发机制
    深入浅出理解消息的传递和转发机制
    消息转发机制原理和实际用途

    image.png image.png

    一、消息传递过程

    objc_msgSend()函数会依据接收者(调用方法的对象)的类型和选择子(方法名)来调用适当的方法。
    1、接收者会根据isa指针找到接收者自己所属的类,然后在该类的缓存中查找对应的IMP,如果找到了,则根据IMP指针跳转到方法的实现代码,调用这个方法的实现;如果没有缓存则初始化缓存,进入步骤2

    方法缓存:
    发现调用一个方法并不像我们想的那么简单,更不像我们写的那么简单,一个方法的执行其实底层需要很多步骤。
    正因如此,objc_msgSend()会将调用过且匹配到的方法缓存在”快速映射表(fast map)“中,快速映射表就是方法的缓存表。每个类都有这样一个缓存。
    所以,即便子类实例从父类的方法列表中取过了某个对象方法,那么子类的方法缓存表中也会缓存父类的这个方法,下次调用这个方法,会优先去当前类(对象所属的类)的方法缓存表中查找这个方法,这样的好处是显而易见的,减少了漫长的方法查找过程,使得方法的调用更快。
    同样,如果父类实例对象调用了同样的方法,也会在父类的方法缓存表中缓存这个方法。
    同理,如果用一个子类对象调用某个类方法,也会在子类的metaclass里缓存一份。而当用一个父类对象去调用那个类方法的时候,也会在父类的metaclass里缓存一份。

    2、在所属类的”方法列表“(method list)中从上向下遍历。如果能找到与选择子名称相符的方法,就根据IMP指针跳转到方法的实现代码,调用这个方法的实现。
    3、如果找不到与选择子名称相符的方法,接收者会根据所属类的superClass指针,沿着类的继承体系继续向上查找(向父类查找),如果能找到与名称相符的方法,就根据IMP指针跳转到方法的实现代码,调用这个方法的实现。
    4、如果在继承体系中还是找不到与选择子相符的方法,此时就会执行”消息转发(message forwarding)“操作。

    二、消息转发过程

    image.png

    Q:说一下你理解的消息转发机制?

    解说如下:

    先会调用objc_msgSend方法,根据消息接收者对象的isa指针,找到接收者对象所属的类Class,首先在Class的缓存中查找IMP,没有缓存则初始化缓存。如果没有找到,则通过Class的superClass指针向父类的Class查找。如果一直查找到根类【即在继承体系中查找】仍旧没有实现,则执行消息转发。

    当遇到一个方法调用,编译器会生成一个objc_msgSend的调用,有4种:
    objc_msgSend:其他的消息会使用
    objc_msgSend_stret:
    objc_msgSendSuper: 发送给父类的message会使用
    objc_msgSendSuper_stret:
    如果方法的返回值是一个结构体(structures),那么就会使用objc_msgSendSuper_stret或者objc_msgSend_stret。

    1、动态方法解析:

    调用resolveInstanceMethod:方法。允许用户在此时为该Class动态添加实现。如果有实现了,则调用并返回YES,重新开始objc_msgSend流程。这次对象会响应这个选择器,一般是因为它已经调用过了class_addMethod。如果仍没有实现,继续下面的动作。

    在当前类中重写此方法:
    void gotoSchool(id self,SEL _cmd,id value) {
        printf("go to school");
    }
    //第一步:对象在收到无法解读的消息后,首先将调用所属类的该方法。
    //这个函数在运行时(runtime),没有找到SEL的IML时就会执行。
    //这个函数是给类利用class_addMethod添加函数的机会。
    //根据文档,如果实现了添加函数代码则返回YES,未实现返回NO。
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        NSString *selectorString = NSStringFromSelector(sel);
        if ([selectorString isEqualToString:@"gotoschool"]) {
            class_addMethod(self, sel, (IMP)gotoSchool, "@@:");
        }
        return [super resolveInstanceMethod:sel];
    }
    

    如果运行期系统已经执行完了动态方法解析,那么消息接受者自己就无法再以动态新增方法的形式来响应包含该未知选择子的消息了,此时就进入了第二阶段——完整的消息转发。运行期系统会请求消息接受者以其他手段来处理与消息相关的方法调用。

    2、备援接收者

    调用forwardingTargetForSelector:方法,尝试找到一个能响应该消息的对象。如果获取到,则直接把消息转发给它,返回非nil对象。否则返回nil,继续下面的动作。注意这里不要返回self,否则会形成死循环。

    //第三步:备援接收者,让其他对象进行处理
    - (id)forwardingTargetForSelector:(SEL)aSelector {
        NSString *selectorString = NSStringFromSelector(aSelector);
        if ([selectorString isEqualToString:@"gotoschool"]) {
            return self.student;
        }
        return nil;
    }
    

    3、完整的消息转发

    3.1 调用methodSignatureForSelector:方法,尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector抛出异常。如果能获取,则返回非nil;传给一个NSInvocation并传给forwardInvocation:。

    对一个你的对象不识别的消息进行响应,你必须重写methodSignatureForSelector:方法,该方法返回一个NSMethodSIgnature对象,该对象包含了给定选择器所标识方法的描述(如:方法名SEL、方法参数、方法返回值、接收者等信息)。主要包含返回值的信息和参数信息。

    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
        NSMethodSignature *sign = [NSMethodSignature signatureWithObjCTypes:"@@:"];
        return sign;
    }
    

    3.2 调用forwardInvocation:方法,将上一步获取到的方法签名包装成Invocation传入,如何处理就在这里面了,并返回非nil。

    - (void)forwardInvocation:(NSInvocation *)anInvocation {
        NSLog(@"%@ can't handle by People",NSStringFromSelector([anInvocation selector]));
    }
    

    forwardInvocation:真正执行从methodSignatureForSelector:返回的NSMethodSignature。在forwardInvocation:函数里可以将NSInvocation多次转发到多个对象中,这也是这种方式灵活的地方。(forwardingTargetForSelector只能以Selector的形式转向一个对象)

    // 第一步:我们不动态添加方法,返回NO,进入第二步;
    + (BOOL)resolveInstanceMethod:(SEL)sel
    {
        return NO;
    }
    
    // 第二部:我们不指定备选对象响应aSelector,进入第三步;
    - (id)forwardingTargetForSelector:(SEL)aSelector
    {
        return nil;
    }
    
    // 第三步:返回方法选择器,然后进入第四部;
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
    {
        if ([NSStringFromSelector(aSelector) isEqualToString:@"sing"]) {
            return [NSMethodSignature signatureWithObjCTypes:"v@:"];
        }
    
        return [super methodSignatureForSelector:aSelector];
    }
    
    // 第四部:这步我们修改调用方法
    - (void)forwardInvocation:(NSInvocation *)anInvocation
    {
        [anInvocation setSelector:@selector(dance)];
        // 这还要指定是哪个对象的方法
        [anInvocation invokeWithTarget:self];
    }
    
    // 若forwardInvocation没有实现,则会调用此方法
    - (void)doesNotRecognizeSelector:(SEL)aSelector
    {
        NSLog(@"消息无法处理:%@", NSStringFromSelector(aSelector));
    }
    
    - (void)dance
    {
        NSLog(@"跳舞!!!come on!");
    }
    
    

    消息派发系统触发消息前,会以某种方式改变消息内容,包括但不限于额外追加一个参数、改变选择子等。
    实现此方法时,如果发现调用操作不应该由本类处理,则需要沿着继承体系,调用父类的同名方法,这样一来,继承体系中的每个类都有机会处理这个调用请求,直至rootClass,也就是NSObject类。
    如果最后调用了NSObject的类方法,那么该方法还会继而调用”doesNotRecognizeSelector:“以抛出异常,此异常表明选择子最终也未能得到处理。消息转发到此结束。

    3.3调用doesNotRecognizeSelector:,默认的实现是抛出异常。如果第三步没能获得一个方法签名,执行该步骤 。

    扩展

    1、对象调用method代码示例

    iOS 使用NSMethodSignature和 NSInvocation进行 method 或 block的调用
    一个实例对象可以通过三种方式调用其方法。

    - (void)test{
        
    //type1
        [self printStr1:@"hello world 1"];
        
    //type2
        [self performSelector:@selector(printStr1:) withObject:@"hello world 2"];
        
    //type3
        //获取方法签名
        NSMethodSignature *sigOfPrintStr = [self methodSignatureForSelector:@selector(printStr1:)];
        
        //获取方法签名对应的invocation
        NSInvocation *invocationOfPrintStr = [NSInvocation invocationWithMethodSignature:sigOfPrintStr];
        
        /**
        设置消息接受者,与[invocationOfPrintStr setArgument:(__bridge void * _Nonnull)(self) atIndex:0]等价
        */
        [invocationOfPrintStr setTarget:self];
        
        /**设置要执行的selector。与[invocationOfPrintStr setArgument:@selector(printStr1:) atIndex:1] 等价*/
        [invocationOfPrintStr setSelector:@selector(printStr1:)];
        
        //设置参数 
        NSString *str = @"hello world 3";
        [invocationOfPrintStr setArgument:&str atIndex:2];
        
        //开始执行
        [invocationOfPrintStr invoke];
    }
    
    - (void)printStr1:(NSString*)str{
        NSLog(@"printStr1  %@",str);
    }
    

    2、ObjcTypes

    它是一个是字符串数组,该数组包含了方法的类型编码。
    如:"v@:@"。
    那究竟是如何得来该字符串呢?其实我们有两种方式:

    1. 直接查表。在Type Encodings里面列出了对应关系。
    2. 使用 @encode()计算。( NSLog(@"%s",@encode(BOOL))的结果为B )

    在OC中,每一种数据类型可以通过一个字符编码来表示(Objective-C type encodings)。例如字符‘@’代表一个object, 'i'代表int。 那么,由这些字符组成的字符数组就可以表示方法类型了。
    举个例子:printStr1:对应的ObjCTypes 为 v@:@。

    // 第三步:返回方法选择器,然后进入第四部;
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
    {
        if ([NSStringFromSelector(aSelector) isEqualToString:@"sing"]) {
            return [NSMethodSignature signatureWithObjCTypes:"v@:"];
        }
    
        return [super methodSignatureForSelector:aSelector];
    }
    
    ’v‘ : void类型,第一个字符代表返回值类型
    ’@‘ : 一个id类型的对象,第一个参数类型
    ’:‘ : 对应SEL,第二个参数类型
    ’@‘ : 一个id类型的对象,第三个参数类型,也就是- (void)printStr1:(NSString*)str中的str。
    

    消息发送会被转换成objc _ msgSend(id reciever,SEL sel,prarams1,params2,....)。所以上面的:

    - (void)printStr1:(NSString*)str{
        NSLog(@"printStr1  %@",str);
    }
    
    [zhagnsan printStr1:lisi]
    //方法会被转换成
    void objc_msgSend(zhangsan,@selector(printStr1:),lisi);   //包含两个隐藏参数
    

    这里的 “v@:@”就代表:

    "v":代表返回值void
    "@":代表一个对象,这里指代的id类型zhangsan,也就是消息的receiver
    ":":代表SEL
    "@":代表参数lisi

    相关文章

      网友评论

          本文标题:runtime-消息传递与转发机制

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