美文网首页
iOS runtime--消息转发

iOS runtime--消息转发

作者: 爬树的蚂蚁 | 来源:发表于2019-02-22 09:50 被阅读10次

    消息转发概述

    Objective-C是一门动态语言,怎么理解动态这一词呢?简单的说就是编译器在编译期可以只知道一个方法的名字,而不需要知道这个方法的实现,只有在运行期间调用该方法的时候,才根据方法名去找到对应方法的实现,这个过程相当于动态绑定一个方法的实现,这就是“动态”。

    与“动态”相对的是“静态”,C语言就是一门静态语言,在编译期不仅知道运行时所要调用的函数的名字,而且直接生成了调用函数的指令,将函数地址硬编码在这些指令中。这就是为什么OC的方法没有实现只有声明编译不报错,而C的方法却报错的原因。

    正因为是动态绑定,所以在编译期没有报错的程序,在运行时由于根据方法名找不到对应的方法实现,会导致程序的Crash,在Crash之前程序会依次调用几个其他的方法,这就引出了消息转发。

    消息转发过程

    消息转发是Objective-C语言的特点,当一个对象在运行时接收到无法解读的消息时,就会触发“消息转发”。

    在编译期向类发送无法解读的消息是不会报错的,因为在运行期可以继续向类添加方法,所以在编译期,编译器无法知道到底有没有某个方法的实现。
    可能听起来有点乱,又是向对象发送无法解读的消息,又是向类发送无法解读的消息。前者可理解为只在运行期有的行为(因为有对象,那肯定是调用了生成对象方法的实现才可能存在,而只有运行期才去调用方法的实现),后者看下面这句代码就知道了

    [self performSelector:@selector(humenName)];
    

    方法“humenName”我并没有声明也没有实现,但是这句代码不会报错(会报警告),编译也可以通过,这就是所谓的编译期向类发送无法解读的消息。

    消息转发的过程是有一定的规则和步骤的。下面我们看看详细的流程。

    1.先看一个runtime库的方法 class_addMethod

    class_addMethod的用处是在程序运行时,给一个类添加方法实现的API,其完整API如下:

    /** 
     * 根据指定的名字和方法实现给一个类添加方法.
     * 
     * @param cls 被添加方法的类.
     * @param name 指定要添加的方法名称的选择器。
     * @param imp 新的方法实现,这个方法必须至少带有两个参数:self和_cmd
     * @param types 上面那个新方法的参数的类型编码. 
     * 
     * @return 当方法添加成功返回YES , 否则返回NO 
     *  (例如,在类中已经有一个该名字的方法实现,会返回NO)
     *
     * @note class_addMethod 可能会覆盖超类实现, 如果超类也实现了该方法的话,
     * 但是不会替换在本类中已经存在的方法实现,
     * 如果改变本类存在的方法实现,请使用method_setImplementation.
     */
    OBJC_EXPORT BOOL
    class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                    const char * _Nullable types) 
    

    这个API在消息转发中用到。消息转发分为三个阶段,即“动态方法解析”、“快速消息转发”和“完整消息转发机制”。

    2. 动态方法解析

    这里结合一个实例来说明,可能会更加容易懂些,新建一个项目,创建一个类HumenModel(继承自NSObject),然后在ViewController中添加如下代码

    HumenModel *model = [HumenModel new];
    [model performSelector:@selector(humenName)];
    

    因为在HumenModel类中并没有方法humenName的声明和实现,所以,对象model会接受到一个无法解析的消息,此时就会进入消息转发的第一阶段,征询接收者所属的类,看是否能动态添加方法,以处理这个未知的“选择器”,此时就调用该类的类方法

    +(BOOL)resolveInstanceMethod:(SEL)sel
    

    当然,如果是类接收到一个无法解析的消息,消息转发第一阶段调用的是类的另一个方法:

    +(BOOL)resolveClassMethod:(SEL)sel
    

    方法的参数 sel 就是那个未知“选择器”,返回值是BOOL类型,表示是否新增一个方法来处理未知“选择器”。 现在我们就可以通过class_addMethod方法给类添加一个方法来处理未知“选择器”,代码如下:

    +(BOOL)resolveInstanceMethod:(SEL)sel{
        
        // 获取选择器的方法名字
        NSString *selString = NSStringFromSelector(sel);
        if ([selString isEqualToString:@"humenName"]) {
            
            // 给接收者self 添加一个方法sayHello,选择器sel指向方法的实现,方法的类型编码是v@:
            class_addMethod(self, sel, (IMP)sayHello, "v@:");
            return YES;
        }
        return [super resolveInstanceMethod:sel];
    }
    // 一个c函数
    void sayHello(id self, SEL _cmd){
        NSLog(@"hello");
    }
    

    解释下class_addMethod方法的第四个参数“v@:”
    这是sayHello方法的OC类型编码,‘v’表示返回值为void类型,‘@’表示第一个参数是对象,‘:’表示第二个参数是SEL类型的值,其中'@'和':'是固定的,因为每个方法都会有这两个参数。对于其他情况可参照类型编码这篇博客https://blog.csdn.net/ssirreplaceable/article/details/53376915

    注意,所添加的方法必须是纯C函数实现的,因为OC的方法名规则和C函数名规则差别是很大的。另外,在运行时,方法resolveInstanceMethod中的代码会被动态插在类里面.

    编译运行,打印结果如下:

    2019-02-20 17:31:53.411603+0800 MessageTrans[400:8535618] hello
    

    跟预期的结果一样,这样就完成了动态方法解析,无法解析的消息在这一步得到了处理。如果在这阶段没有针对未知“选择器”的做出处理,那么就会进入消息转发的第二阶段。

    3.快速消息转发机制

    在这一阶段,接收者将要甩锅,看有没有别的接收者可以处理这个无法解析的消息(记得将第一阶段的代码注释掉)。这个一过程会在接收者所在类的下面这个方法中完成

    -(id)forwardingTargetForSelector:(SEL)aSelector
    

    参数aSelector是未知选择器,返回值是id类型的值,所以这一阶段只是针对对象来处理,不考虑类方法。

    新建一个AnimalModel类,在其实现文件中实现方法humenName,如下:

    -(void)humenName{
        NSLog(@"%s",__FILE__);
    }
    

    然后在HumenModel.m中实现forwardingTargetForSelector:方法,如下:

    -(id)forwardingTargetForSelector:(SEL)aSelector{
        NSString *aSelectorString = NSStringFromSelector(aSelector);
        if ([aSelectorString isEqualToString:@"humenName"]) {
            return [AnimalModel new];
        }
        
        return [super forwardingTargetForSelector:aSelector];
    }
    

    这样消息交由AnimalModel类处理,运行一下,打印结果如下:

    2019-02-20 17:36:49.000246+0800 MessageTrans[2513:8550047] /Users/xiangzuhua/Desktop/MessageTrans/MessageTrans/AnimalModel.m
    

    结果正确,我们在第二阶段成功的将消息转发给其他接收者来处理。

    对于AnimalModel类,不需要在其头文件中声明humenName方法,没有影响。

    如果没有其他接收者,那就会进入“完整消息转发机制”阶段。

    4.完整消息转发机制

    在这个阶段要处理未知消息,代价就会大些,其实也是类似于快速消息转发阶段,目的都是指定一个接受消息的对象,只不过这里必须覆盖两个方法,即methodSignatureForSelector:和forwardInvocation:。

    methodSignatureForSelector:的作用在于为另一个类实现的消息创建一个有效的方法签名,必须实现,并且返回不为空的methodSignature,否则会crash。
    forwardInvocation:的作用是绑定消息接收者。

    看代码实现:

    -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
        
        if ([super methodSignatureForSelector:aSelector] == nil) {
            // 手动创建
            NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
            return signature;
        }
        return [super methodSignatureForSelector:aSelector];
        
        // 自动创建方法签名
    //    AnimalModel *animalModel = [AnimalModel new];
    //    return [animalModel  methodSignatureForSelector:aSelector];
    }
    
    -(void)forwardInvocation:(NSInvocation *)anInvocation{
        AnimalModel *model = [AnimalModel new];
        
        if ([model respondsToSelector:anInvocation.selector]) {
            [anInvocation invokeWithTarget:model];
        }else{
            [self doesNotRecognizeSelector:anInvocation.selector];
        }
    }
    

    解释下上面的代码:方法签名有两种方式,一个是手动创建签名,一个是自动创建签名,看代码可以明白,着重要讲的是下面这句

    NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
    

    参数值"v@:"为类型编码,前面有提到,这里就不说规则了。原则上来说,该编码的类型应该与未知方法的参数对应,所以用根据未知方法自动创建方法签名更好。但是如果非要用手动创建方法签名的话,在写方法signatureWithObjCTypes:的参数值时要注意两点

    • 方法的返回类型必须有,不能省略,比如这里是返回空类型,所以对应第一个编码为v
    • 方法的默认参数self和_cmd对应的类型编码不能写错,固定为"@:"

    满足上面两点签名就会有效,否则会导致crash,至于方法签名中参数个数与未知方法不对应是没有问题的,比如说类型编码为"v@:@@@",而未知方法为:humenName(没有参数),是不会导致crash,只不过这里的参数写多少个,会影响方法forwardInvocation:的参数值anInvocation的变化。

    在第二个方法forwaidInvocation:必须判断接收者是否能响应未知消息,否则直接执行[anInvocation invokeWithTarget:model]在接收者无法响应位置消息时会导致崩溃。如果指定的接收者不能响应未知选择器,那么没办法了只能抛出异常,执行doesNotRecognizeSelector:方法,程序崩溃。

    这里在讲下刚才提到的,方法签名的参数个数的问题,看下面的代码:

    -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
        
        if ([super methodSignatureForSelector:aSelector] == nil) {
            // 手动创建
            NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"@@:@@"];
            return signature;
        }
        return [super methodSignatureForSelector:aSelector];
        
        // 自动创建方法签名
    //    AnimalModel *animalModel = [AnimalModel new];
    //    return [animalModel  methodSignatureForSelector:aSelector];
    }
    
    -(void)forwardInvocation:(NSInvocation *)anInvocation{
        AnimalModel *model = [AnimalModel new];
        
        SEL sel = anInvocation.selector;
        NSMethodSignature *sign = anInvocation.methodSignature;
        NSLog(@"%lu--%@",(unsigned long)sign.numberOfArguments,NSStringFromSelector(sel));
        
        if ([model respondsToSelector:anInvocation.selector]) {
            [anInvocation invokeWithTarget:model];
        }else{
            [self doesNotRecognizeSelector:anInvocation.selector];
        }
    }
    

    AnimalModel类中humenName方法实现如下:

    -(void)humenName{
        NSLog(@"%s",__FILE__);
    }
    

    运行打印结果如下

    2019-02-21 15:31:41.851617+0800 MessageTrans[14393:9247960] 4--humenName
    2019-02-21 15:31:48.716371+0800 MessageTrans[14393:9247960] /Users/xiangzuhua/Desktop/MessageTrans/MessageTrans/AnimalModel.m
    

    根据打印结果可以看到,方法签名中参数个数,与anInvocation中的参数个数值是对应的,并不与未知选择器humenName的参数个数对应(参数个数为0),而且这种不对应并不会影响AnimalModel类中方法humenName的正常执行。

    拓展:如果对象调用一个未指定参数值的未知消息,但是在另一个类中有该方法的实现,会怎样呢
    刚才我们调用的未知方法是没有参数的,我们实现下有参数不指定值得代码
    viewController.m -> viewDidLoad

    HumenModel *model = [HumenModel new];
    [model performSelector:@selector(humenName:)];
    

    AnimalModel.m

    -(void)humenName:(NSInteger)number{
       NSLog(@"%s - %ld",__FILE__,(long)number);
    }
    

    HumenModel.m中的代码和上面的相同,编译运行,发现崩溃了,原因容易想到是这个消息没有参数值,当在AnimalModel中调用humenName:时,就会报野指针。这个问题怎么解决呢,见下面的代码

    -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
       
       // 自动创建方法签名
       AnimalModel *animalModel = [AnimalModel new];
       return [animalModel  methodSignatureForSelector:aSelector];
    }
    
    -(void)forwardInvocation:(NSInvocation *)anInvocation{
       AnimalModel *model = [AnimalModel new];
    // 给anInvocation设置参数值
       NSInteger number = 10;
       [anInvocation setArgument:&number atIndex:2];// 为什么是2,因为0是self参数,1是_cmd参数,返回值类型不属于参数。
       
       if ([model respondsToSelector:anInvocation.selector]) {
           [anInvocation invokeWithTarget:model];
       }else{
           [self doesNotRecognizeSelector:anInvocation.selector];
       }
    }
    

    这样再运行看结果如下:

    2019-02-21 16:13:56.024182+0800 MessageTrans[15139:9320442] /Users/xiangzuhua/Desktop/MessageTrans/MessageTrans/AnimalModel.m - 10
    

    运行正常

    到这一步消息转发的整个流程就讲完了。

    runtime底层代码

    消息转发的实际应用

    1.多重代理
    2.多重继承
    3.为 @dynamic修饰的属性实现方法

    相关文章

      网友评论

          本文标题:iOS runtime--消息转发

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