美文网首页
runtime - 消息转发

runtime - 消息转发

作者: 啊啊啊啊锋 | 来源:发表于2016-07-06 16:18 被阅读37次

    通过前边的学习我们知道,某个类或者对象调某个方法实际上就是给这个类/对象发送消息,如果我们某个对象要调用某个方法,而这个方法又没有实现的时候,编译器就会报错:>

    2016-07-06 15:19:27.657 ZFRuntime[1430:172708] -[Dog eat]: unrecognized selector sent to instance 0x7fa15b7043b0
    

    今天我们就从这个错误开始入手,一步步分析runtime的消息转发机制!

    先来了解下objc_msgSend函数调用检测过程:

    我们首先知道,当我们调用一个对象的一个方法的时候,会通过*isa真寻找本类中对于该方法的实现IMP,如果本类找不到,则会向父类寻找,当走到NSObject都无法调用时,程序会crash掉。但是在crash之前,运行时为我们提供了一个机制就是可以动态地增加这个方法的实现或者让另一个对象来响应这个方法。

    用步骤表示下就是:
    1.检测这个selector是不是要忽略的
    2.检测这个target是不是nil对象,nil对象执行任何方法都不会崩溃,因为会被忽略掉
    3.查找这个类的IMP(方法实现),先从方法缓存列表中查找,若找到则跳转到对应的函数去执行;若找不到则查找方法分发表。如果分发表找不到就到父类分发表查找,知道找到或者查找到NSObjct根类为止
    4.如果前三步都找不到,那么开始进入方法动态解析

    方法动态解析

    来看下下边这张图:


    方法动态解析

    方法动态解析流程是这样的:

    1.首先看+ (BOOL)resolveInstanceMethod:(SEL)sel (或者+ (BOOL)resolveClassMethod:(SEL)sel)有没有实现,如果返回YES,则通过class_addMethod函数动态地添加方法,消息得到处理,此流程完毕;如果返回NO,则进入下一步
    2.在第一步返回NO的情况下,进入- (id)forwardingTargetForSelector:(SEL)aSelector方法,用于指定哪个对象响应这个selector(注意不能指定为self),若返回某个对象,则调用该对象的方法;若返回nil,则进入下一步
    3.在第二步返回nil的情况下,首先通过- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector指定方法签名,若返回nil,则不处理;如返回犯法签名,则进入下一步
    4.在第三步返回方法签名的情况下,就会调用- (void)forwardInvocation:(NSInvocation *)anInvocation方法,在这个方法里边我们可以做很多处理,比如修改方法实现,修改方法实现的类等等
    5.如果没有实现第四步的方法,则会调用- (void)doesNotRecognizeSelector:(SEL)aSelector方法,如果没有实现这个方法那么就会crash,并提示找不到响应方法。至此,动态解析流程结束

    我们创建三个类,并且这三个类分别:
    1.提供声明,但是不提供方法实现。验证当找不到方法的实现时,动态添加方法
    2.不提供声明,将调用对象修改成其它类实例。验证修改处理消息的对象
    3.不提供声明,不修改调用对象,但是修改调用的方法

    例一

    Dog.h
    @interface Dog : NSObject
    // 在这里我们只声明,不实现
    - (void)eat;
    @end
    
    Dog.m
    /** 
     *  此函数指定是否动态添加方法,返回NO则继续向下执行,返回YES则根据class_addMethod动态添加方法,此流程执行完毕
     *  注意:这个函数调用的前提是我们之前声明的方法并没有实现IMP(eat方法未实现),程序才会到这个地方检测是否动态添加了方法
     *  这个方法实际上是给用户一个机会动态地添加一个方法
     */
    + (BOOL)resolveInstanceMethod:(SEL)sel
    {
        if ([NSStringFromSelector(sel) isEqualToString:@"eat"])
        class_addMethod([self class], sel, (IMP)eat, "v@:");
        // 如果我们要调用的这个方法是`eat`方法,那么返回YES,就是要告诉编译器`eat`这个方法我们自己实现了,请直接调用
        return YES;
        return NO;
    }
    
    /**
     *  编译器在将函数转换成objc_msgSend函数调用时,都会自动添加上(id self, SEL cmd)这两个参数,因此我们就可以拿得到
     */
    void eat(id self, SEL cmd)
    {
        NSLog(@"%@ is eating", self);
    }
    

    因为我们并没有提供eat方法的实现,因此当我们调用Dog对象的eat方法时,编译器就会调用+ (BOOL)resolveInstanceMethod:(SEL)sel这个方法,允许我们动态地添加方法。好了我们在ViewController里边测试下:

    Dog *dog = [Dog new];
    [dog eat];
    

    打印结果如下:

    2016-07-06 16:05:48.122 ZFRuntime[1655:224004] <Dog: 0x78f79530> is eating
    

    从打印结果看,说明我们成功地添加了自定义的eat方法的实现作为- eat方法的实现。

    例二

    创建一个Pig

    Pig.h
    @interface Pig : NSObject
    @end    
    
    Pig.m
    #import "Pig.h"
    #import "Dog.h"
    
    @implementation Pig
    
    // 1、不动态添加方法
    + (BOOL)resolveInstanceMethod:(SEL)sel
    {
        return NO;
    }
    
    // 2、备选提供相应aSelector的对象,此处不备选,进入第3步
    - (id)forwardingTargetForSelector:(SEL)aSelector
    {
        return nil;
    }
    
    // 3、先返回方法选择器,如果返回nil,则表示消息无法处理
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
    {
        if ([NSStringFromSelector(aSelector) isEqualToString:@"eat"]) {
            return [NSMethodSignature signatureWithObjCTypes:"v@:"];
        }
        return [super methodSignatureForSelector:aSelector];
    }
    
    // 4、只有返回了方法签名才会走到这一步,这一步用户调用方法
    - (void)forwardInvocation:(NSInvocation *)anInvocation
    {   // 在这里修改调用‘eat’方法的对象为Dog对象
        [anInvocation invokeWithTarget:[[Dog alloc] init]];
    }
    
    /** 备注:
     *  如果我们调用‘resolveInstanceMethod:’返回NO,那么我们就没必要实现他,因为默认返回NO
     *  如果我们调用‘forwardingTargetForSelector:’返回nil,那么我们就没必要实现他,因为默认返回nil
     *  但是对于‘methodSignatureForSelector:’默认也是返回nil,如果我们不返回某方法签名,那么
     *  ‘forwardInvocation:’方法就不会被调用,此时也就崩溃了
     */
    
    @end
    

    由于我们并没有在Pig.h文件里边定义- eat方法,因此我们不能直接调用它的eat方法来进行测试,但是我们可以调用performSelector:方法来简介调用eat方法。好了我们在ViewController里边测试下:

    Pig *pig = [Pig new];
    // 1、可以这样调用方法
    [pig performSelector:@selector(eat)];
    // 2、或者这样调用
    ((void (*)(id, SEL)) objc_msgSend)((id)pig, @selector(eat));
    

    打印结果如下:

        2016-07-06 16:05:48.122 ZFRuntime[1655:224004] <Dog: 0x78f79530> is eating
    

    打印结果是Dog,说明我们成功地修改调用对象

    例三

    创建一个Cat

    Cat.h
    @interface Cat : NSObject
    @end
    
    Cat.m
    + (BOOL)resolveInstanceMethod:(SEL)sel
    {
        return NO;
    }
    
    - (id)forwardingTargetForSelector:(SEL)aSelector
    {
        return nil;
    }
    
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
    {
        if ([NSStringFromSelector(aSelector) isEqualToString:@"eat"]) {
            return [NSMethodSignature signatureWithObjCTypes:"v@:"];
        }
        return [super methodSignatureForSelector:aSelector];
    }
    
    /**
     *  当实现了这个方法,‘doesNotRecognizeSelector:’就不会被调用
     */
    - (void)forwardInvocation:(NSInvocation *)anInvocation
    {
        // 改变方法选择器
        [anInvocation setSelector:@selector(jump)];
        // 指定哪个对象实现这个方法
        [anInvocation invokeWithTarget:self];
    }
    
    - (void)jump
    {
        NSLog(@"由eat方法改成jump方法");
    }
    
    - (void)doesNotRecognizeSelector:(SEL)aSelector
    {
        NSLog(@"无法处理消息:%@", NSStringFromSelector(aSelector));
    }
    
    /** 注意:
     *  当我们实现了‘doesNotRecognizeSelector:’方法是,就不会因为找不到方法而崩溃了,这里我动态地将‘eat’方法
     *  修改成‘jump’方法,同时也要设置这个‘jump’是哪个对象实现的
     */
    

    好了我们在ViewController里边测试下:

    Cat *cat = [Cat new];
    [cat performSelector:@selector(eat)];
    

    打印结果如下:

    2016-07-06 16:15:53.199 ZFRuntime[1829:234594] 由eat方法改成jump方法
    

    说明我们已经成功地动态地修改方法了。

    本文学习笔记根据这篇文章整理而来,感谢原作者

    Demo地址

    相关文章

      网友评论

          本文标题:runtime - 消息转发

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