iOS 消息转发

作者: 1剑天下 | 来源:发表于2017-08-03 23:47 被阅读81次

    消息

    runtime部分参考 http://www.cnblogs.com/ioshe/p/5489086.html

    体会苹果官方文档中的 messages aren’t bound to method implementations until Runtime。消息直到运行时才会与方法实现进行绑定。

    这里要清楚一点,objc_msgSend 方法看清来好像返回了数据,其实objc_msgSend 从不返回数据,而是你的方法在运行时实现被调用后才会返回数据。下面详细叙述消息发送的步骤(如下图):

    1330553-87d03a7c0971c730.gif
    1. 首先检测这个 selector 是不是要忽略。比如 Mac OS X 开发,有了垃圾回收就不理会 retain,release 这些函数。
    2. 检测这个 selector 的 target 是不是 nil,Objc 允许我们对一个 nil 对象执行任何方法不会 Crash,因为运行时会被忽略掉。
    3. 如果上面两步都通过了,那么就开始查找这个类的实现 IMP,先从 cache 里查找,如果找到了就运行对应的函数去执行相应的代码。
    4. 如果 cache 找不到就找类的方法列表中是否有对应的方法。
    5. 如果类的方法列表中找不到就到父类的方法列表中查找,一直找到 NSObject 类为止。
    6. 如果还找不到,就要开始进入动态方法解析了,后面会提到。

    什么是消息转发

    打个比方:快递员送快递的时候,本人不在,快递员就会将包裹交给你熟悉的人进行签收 这个包裹就是消息

    当Object-C 中调用某一个方法的时候,首先系统回去查看这个方法是否存在,或者是有没有实现这个方法,如果不存在或者没有实现的情况下,系统会调用一些方法,为你提供补救的机会,你可以利用这些方法进行消息转发,防止程序Crash

    1330553-400bc5bde4db1725.png

    一些 Runtime 的术语的数据结构

    SEL

    它是selector在 Objc 中的表示(Swift 中是 Selector 类)。selector 是方法选择器,其实作用就和名字一样,日常生活中,我们通过人名辨别谁是谁,注意 Objc 在相同的类中不会有命名相同的两个方法。selector 对方法名进行包装,以便找到对应的方法实现。它的数据结构是:

    typedef struct objc_selector *SEL;
    我们可以看出它是个映射到方法的 C 字符串,你可以通过 Objc 编译器器命令@selector() 或者 Runtime 系统的 sel_registerName 函数来获取一个 SEL 类型的方法选择器。

    NSMethodSignature

    顾名思义应该就是“方法签名”,类似于C++中的编译器时的函数签名。苹果官方定义该类为对方法的参数、返回类似进行封装,协同NSInvocation实现消息转发。通过消息转发实现类似C++中的多重继承。

    NSInvocation

    iOS中可以直接调用 某个对象的消息 方式有2种
    一种是performSelector:withObject:另一种就是NSInvocation 调用

    // 方法签名中保存了方法的名称/参数/返回值,协同NSInvocation来进行消息的转发
    // 方法签名一般是用来设置参数和获取返回值的, 和方法的调用没有太大的关系
    //1、根据方法来初始化NSMethodSignature
    NSMethodSignature  *signature = [ViewController instanceMethodSignatureForSelector:@selector(run:)];
    // NSInvocation中保存了方法所属的对象/方法名称/参数/返回值
    //其实NSInvocation就是将一个方法变成一个对象
    //2、创建NSInvocation对象
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
    //设置方法调用者
    invocation.target = self;
    //注意:这里的方法名一定要与方法签名类中的方法一致
    invocation.selector = @selector(run:);
    NSString *way = @"byCar";
    //这里的Index要从2开始,以为0跟1已经被占据了,分别是self(target),selector(_cmd)
    [invocation setArgument:&way atIndex:2];
    //3、调用invoke方法
    [invocation invoke];
    //实现run:方法
    
    - (void)run:(NSString *)method{
    
    }
    
    

    消息转发方案

    防止程序crash,可以用消息转发的方案有以下的三种方案,你只能使用中一种方案来处理,因为,系统发现某一个方法不存在的时候首先会走方案一里面的方法,如果你用方案一解决了,方案二,以及方案三的方法就不会再走了

    XWPerson这个类没有去实现run方法,我们声明一个XWPerson实例,然后调用这个run方法,

     XWPerson * person =[[XWPerson alloc] init];
      [person performSelector:@selector(run)];
    

    方案一

    #import "XWPerson.h"
    #import <objc/runtime.h>
    #import "XWCar.h"
    @implementation XWPerson
    
    
    //方案一
    
    void run(id self,SEL _cmd)
    {
        NSLog(@"方案一消息转发");
    }
    /*
     当启动消息转发机制,首先会到+ (BOOL)resolveInstanceMethod:(SEL)sel它是动态给选择器提供一个实现,如果它返回YES,那么会有对这个方法处理的对象,如果返回NO,将跳到下一个消息,去寻找这个方法的处理者。
     所以为了让我们的方法得到处理我们可以通过运行时动态给他添加方法
      */
    +(BOOL)resolveInstanceMethod:(SEL)sel
    {
        //判断是否是run
        
        if ([@"run" isEqualToString:NSStringFromSelector(sel)]) {
            
            // 动态添加run方法
            //types:一个定义该函数返回值类型和参数类型的字符串,http://www.jianshu.com/p/f4129b5194c0
            // v: 代表返回参数是Void
            // @ 表示参数
            
            class_addMethod(self, sel, (IMP)run,"v@:");
            
            return YES;
        }
        
        return [super resolveInstanceMethod:sel];
    }
    
    E2E5E157-D970-477C-A390-0C94142D0FE5.png

    方案二
    创建一个新的类为XWCar,让这个类来实现run方法,当调用XWPerson的run时,转发给XWCar去调用这个run

    316F518B-6150-474C-B71D-1360C71E8A3C.png
    #import "XWPerson.h"
    #import <objc/runtime.h>
    #import "XWCar.h"
    @implementation XWPerson
    
    //方案二
    //当resolveInstanceMethod返回为NO时,也就是没有通过方案一去处理时,会调用下面这个方法
    //Runtime 系统允许我们替换消息的接收者为其他对象
    -(id)forwardingTargetForSelector:(SEL)aSelector
    {
        return [[XWCar alloc] init];
    }
    
    
    @end
    
    

    方案三
    当为采用方案一也未采用方案二时会调用下面两个方法

    2F6A1B40-358E-40E1-8750-732A5257F15F.png
    // 方案三
    //如果一个对象想要转发它接受的任何远程消息,它得给出一个方法标签来返回准确的方法描述 methodSignatureForSelector:,这个方法会最终响应被转发的消息。从而生成一个确定的 NSInvocation 对象描述消息和消息参数。这个方法最终响应被转发的消息
    -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
    {
        if ([@"run" isEqualToString:NSStringFromSelector(aSelector)]) {
            
    
            // 生成签名 后会自动调用 forwardInvocation 方法
            NSMethodSignature *signature =[NSMethodSignature signatureWithObjCTypes:"v@:"];
            
    
            
            return signature;
        }
        return [super methodSignatureForSelector:aSelector];
    }
    
    -(void)forwardInvocation:(NSInvocation *)anInvocation
    {
    
        NSLog(@"未找到实例方法");
    
        //修改selector
            [anInvocation setSelector:@selector(run3)];
            // 修改target
            [anInvocation invokeWithTarget:[XWCar new]];
    
    }
    
    

    值得注意的是NSMethodSignature是一个方法信号的形式,这里的目的是我们要拿到我们未来调用方法的“样子”,而不是那个方法,并且当我们如果用alloc/init创建这个方法的时候它会给我们返回一个nil,因此需要用NSMethodSignature *signature =[NSMethodSignature signatureWithObjCTypes:"v@:"]才会继续调用forwardInvocation:。

    C94893FF-0709-4131-933C-BFC07ECB8438.png

    相关文章

      网友评论

        本文标题:iOS 消息转发

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