美文网首页
每日一问10——runtime消息转发

每日一问10——runtime消息转发

作者: 巫师学徒 | 来源:发表于2017-09-11 17:09 被阅读10次

    终于要说到重点了,objective-c的这种有趣的语法被苹果称为“发消息”。与其他面向对象语言(C++/Java)的“方法调用”不同,objc的消息机制是由运行时实现、非常灵活动态。

    消息机制

    1.为什么叫发消息

    先来看一段例子:

    [receiver message];
    

    这一句的含义是:向receiver发送名为message的消息。

    clang -rewrite-objc MyClass.m
    

    执行上面的命令,将这一句重写为C代码,是这样的:

    ((void (*)(id, SEL))(void *)objc_msgSend)((id)receiver, sel_registerName("message"));
    

    去掉那些强制转换,最终[receiver message]会由编译器转化为以下的纯C调用。

    objc_msgSend(receiver, @selector(message));
    

    所以说,objc发送消息,最终大都会转换为objc_msgSend的方法调用

    看一下objc_msgSend的声明

    id objc_msgSend(id self, SEL _cmd, ...)
    

    发现这个函数是一个不定参的函数,但有2个确定的参数,一个是id类型的receiver对象,一个是SEL类型的方法选择器_cmd。于是我们可以简单理解,objective-c中调用方法其实就是向指定对象发送一个调用方法的消息。

    2.基本的数据结构

    首先 runtime定义了如下的数据类型:

    typedef struct objc_class *Class;
    typedef struct objc_object *id;
    struct objc_object {
        Class isa;
    };
    struct objc_class {
        Class isa;
    }
     
    /// 不透明结构体, selector
    typedef struct objc_selector *SEL;
     
    /// 函数指针, 用于表示对象方法的实现
    typedef id (*IMP)(id, SEL, ...);
    

    根据之前文章的介绍,我们已经知道了id代表对象,Class代表对象的类,都可以通过指向首地址的isa指针找到。

    SEL

    SEL又叫选择器,是表示一个方法的selector的指针,其定义如下:

    typedef struct objc_selector *SEL;
    

    方法的selector用于表示运行时方法的名字。Objective-C在编译时,会依据每一个方法的名字、参数序列,生成一个唯一的整型标识(Int类型的地址),这个标识就是SEL。如下:

    SEL sel1 = @selector(method1);
    NSLog(@"sel : %p", sel1);
    

    打印结果sel : 0x100002d72
    两个类之间,不管它们是父类与子类的关系,还是之间没有这种关系,只要方法名相同,那么方法的SEL就是一样的。每一个方法都对应着一个SEL。所以在Objective-C同一个类(及类的继承体系)中,不能存在2个同名的方法,即使参数类型不同也不行。相同的方法只能对应一个SEL。

    所以运行时维护着一张SEL的表,将相同字符串的方法名映射到唯一一个SEL。 通过sel_registerName(char *name)方法,可以查找到这张表中方法名对应的SEL。
    本质上,SEL只是一个指向方法的指针(准确的说,只是一个根据方法名hash化了的KEY值,能唯一代表一个方法)

    IMP

    IMP是一个函数指针。objc中的方法最终会被转换成纯C的函数,IMP就是为了表示这些函数的地址。

    id (*IMP)(id, SEL, ...)
    

    第一个参数是指向self的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针),第二个参数是方法选择器(selector),接下来是方法的实际参数列表。

    Method

    还记得之前提到的,在objct_class结构体中有struct objc_method_list **methodLists 的结构体,里面存放的是这个类中所有的方法。
    于是我查阅到具体的一个方法结构体

    struct objc_method {
        SEL method_name                                          OBJC2_UNAVAILABLE;
        char *method_types                                       OBJC2_UNAVAILABLE;
        IMP method_imp                                           OBJC2_UNAVAILABLE;
    }  
    

    我们可以看到该结构体中包含一个SEL和IMP,实际上相当于在SEL和IMP之间作了一个映射。有了SEL,我们便可以找到对应的IMP,从而调用方法的实现代码。

    3.方法调用流程

    先看一下我从YY大神博客扒下来的汇编伪代码

    id objc_msgSend(id self, SEL op, ...) {
        if (!self) return nil;
        IMP imp = class_getMethodImplementation(self->isa, SEL op);
        imp(self, op, ...); //调用这个函数,伪代码...
    }
     
    //查找IMP
    IMP class_getMethodImplementation(Class cls, SEL sel) {
        if (!cls || !sel) return nil;
        IMP imp = lookUpImpOrNil(cls, sel);
        if (!imp) return _objc_msgForward; //这个是用于消息转发的
        return imp;
    }
     
    IMP lookUpImpOrNil(Class cls, SEL sel) {
        if (!cls->initialize()) {
            _class_initialize(cls);
        }
     
        Class curClass = cls;
        IMP imp = nil;
        do { //先查缓存,缓存没有时重建,仍旧没有则向父类查询
            if (!curClass) break;
            if (!curClass->cache) fill_cache(cls, curClass);
            imp = cache_getImp(curClass, sel);
            if (imp) break;
        } while (curClass = curClass->superclass);
     
        return imp;
    }
    

    首先在Class中的缓存查找imp(没缓存则初始化缓存),如果没找到,则向父类的Class查找。如果一直查找到根类仍旧没有实现,则用_objc_msgForward函数指针代替imp。最后,执行这个imp。

    messaging1.gif
    这个流程就和我们之前讨论过的结构体objc_class联系上了。
    Class super_class            //指向父类的结构体                            
    struct objc_method_list **methodLists         //方法method列表            
    struct objc_cache *cache      //方法缓存cache
    

    看到这里,我们可以基本明白objective-c中调用方法到底是怎么实现的了。也知道了所谓的“发消息”到底是怎样一件事情。

    我们知道,调用一个未实现的方法时,正常情况会发生崩溃并报出'-[TestObject xxx]: unrecognized selector sent to instance。即没有找到某个方法的实现。在上面我们提到了,如果一直没有查找到那个方法的实现,则会调用_objc_msgForward。接下来我们就要说一下后续的操作。

    消息转发

    当我们发送一个错误的消息时

    Test *test = [Test new];
    [test performSelector(@selector(xxx))];
    

    看一下具体的方法调用顺序

    + Test NSObject initialize
    + Test NSObject new
    + Test NSObject alloc
    + Test NSObject allocWithZone:
    - Test NSObject init
    - Test NSObject performSelector:
    + Test NSObject resolveInstanceMethod:
    - Test NSObject forwardingTargetForSelector:
    - Test NSObject methodSignatureForSelector:
    - Test NSObject class
    - Test NSObject doesNotRecognizeSelector:
    

    可以看到,当NSObject抛出doesNotRecognizeSelector:时,程序就崩溃了。所以我们必须在这之前对此次消息进行处理。

    1.动态方法解析

    对象在接收到未知的消息时,首先会调用所属类的类方法+resolveInstanceMethod:(实例方法)或者+resolveClassMethod:(类方法)。在这个方法中,我们有机会为该未知消息新增一个”处理方法””。不过使用该方法的前提是我们已经实现了该”处理方法”,只需要在运行时通过class_addMethod函数动态添加到类里面就可以了。

    - (void)viewDidLoad {
        [super viewDidLoad];
        TestObject *test = [TestObject new];
        [test test];
    }
    
    @implementation TestObject
    void functionForTest(id self, SEL _cmd) {
        NSLog(@"%@, %p", self, _cmd);
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        NSString *selString = NSStringFromSelector(sel);
        if([selString isEqualToString:@"test"]) {
            class_addMethod(self.class, @selector(test), (IMP)functionForTest, "@:");
        }
        return [super resolveInstanceMethod:sel];
    }
    @end
    
    2.备用接收者

    如果在上一步无法处理消息,则Runtime会继续调以下方法:

    - (id)forwardingTargetForSelector:(SEL)aSelector
    

    如果一个对象实现了这个方法,并返回一个非nil的结果,则这个对象会作为消息的新接收者,且消息会被分发到这个对象。当然这个对象不能是self自身,否则就是出现无限循环。当然,如果我们没有指定相应的对象来处理aSelector,则应该调用父类的实现来返回结果。

    //备用接受对象
    @implementation helpObject
    - (void)test {
        NSLog(@"help test do");
    }
    @end
    
    @implementation TestObject
    - (id)forwardingTargetForSelector:(SEL)aSelector {
        NSString *selString = NSStringFromSelector(aSelector);
        if([selString isEqualToString:@"test"]) {
            helpObject *help = [helpObject new];
            return help;
        }
        return [super forwardingTargetForSelector:aSelector];
    }
    @end
    
    3.完整的消息转发

    如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。此时会调用以下方法:

    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
    

    尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector抛出异常。

    然后就是我们的最后一道关卡forwardInvocation:
    NSObject的forwardInvocation:方法实现只是简单调用了doesNotRecognizeSelector:方法,它不会转发任何消息。

    从某种意义上来讲,forwardInvocation:就像一个未知消息的分发中心,将这些未知的消息转发给其它对象。

    - (void)forwardInvocation:(NSInvocation *)anInvocation
    

    运行时系统会在这一步给消息接收者最后一次机会将消息转发给其它对象。对象会创建一个表示消息的NSInvocation对象,把与尚未处理的消息有关的全部细节都封装在anInvocation中,包括selector,目标(target)和参数。我们可以在forwardInvocation方法中选择将消息转发给其它对象。

    • forwardInvocation:方法的实现有两个任务:

    • 定位可以响应封装在anInvocation中的消息的对象。这个对象不需要能处理所有未知消息。

    使用anInvocation作为参数,将消息发送到选中的对象。anInvocation将会保留调用结果,运行时系统会提取这一结果并将其发送到消息的原始发送者。

    上面两步只能让这个消息变为正确,而一些复杂的操作可以在这里处理,比如追回一个参数等,然后再去触发消息。另外,若发现某个消息不应由本类处理,则应调用父类的同名方法,以便继承体系中的每个类都有机会处理此调用请求。

    @implementation TestObject {
        helpObject *_help;
    }
    
    - (instancetype)init
    {
        self = [super init];
        if (self) {
            _help = [helpObject new];
        }
        return self;
    }
    
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
        NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
        if(!signature) {
            signature = [_help methodSignatureForSelector:aSelector];
        }
        return signature;
    }
    
    - (void)forwardInvocation:(NSInvocation *)anInvocation {
        [anInvocation invokeWithTarget:_help];
    }
    

    小结

    通过本章,我们知道了runtime消息机制与消息转发究竟是什么样的。通过消息,我们可以给程序添加许多动态行为,让我们的程序更加灵活。

    上面都太官方了,主要学习的东西我觉得是objective-c发消息的具体流程,理解方法调用的原理和系统相关的处理,最后才是利用消息转发实现一些黑魔法。

    相关文章

    Objective-C 中的消息与消息转发
    Objective-C Runtime 运行时之三:方法与消息
    消息转发示例

    相关文章

      网友评论

          本文标题:每日一问10——runtime消息转发

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