美文网首页
iOS runtime 理解消息传递、转发机制和使用案例

iOS runtime 理解消息传递、转发机制和使用案例

作者: 只有时间看 | 来源:发表于2018-03-30 11:49 被阅读123次

    关于runtime,网上的资料都很全了,这里是根据自己的理解写一个学习总结报告。主要借鉴文章如下:
    https://www.jianshu.com/p/db6dc23834e3 神经病院Objective-C Runtime出院第三天——如何正确使用Runtime
    https://www.cnblogs.com/ioshe/p/5489086.html [iOS开发-Runtime详解]

    1.runtime是用来做什么的?

    1.1 与runtime相关最大的,就是OC语言的动态绑定机制。

    动态绑定是指一个对象发送消息后,该消息的实现(实际执行的函数)根据运行环境的不同而不同(此处只针对OC,Swift中已经不是运行时加载方法,而是和C语言类似,在编译阶段就确定了)。实现该机制,常用的就是分类(categor)、类扩展(extension)、子类(subclass)继承等我们每个人都会使用的设计模式。
    正常情况下,我们使用OC的这些特性就能够解决大部分问题。但是有些情况下,为了优雅、高效的解决问题,我们有时候希望从更底层的层面进行操纵。


    1.2. 一个经典案例

    有一个业务需求,我们希望统计某个页面viewController被点击的次数,或者在进入某些页面的时候添加引导图。常规的做法是在这些对应的页面的viewDidLoad中进行对应的需求作业。但是,如果项目比较大,页面非常多,或者层级很复杂,这样操作就效率很低,需要到不同的界面去进行分散的操作,日后新增、修改、维护或者调整也很麻烦。一个比较高效、优雅的做法是在基类UIViewContoller的viewDidLoad中实现该方法,因为所有的页面都会继承基类的viewDidLoad方法,在该基类中实现之后我们只需要在此处维护和新增就够了。
    所以我们需要给UIViewContoller基类添加一个category分类,在分类中重写viewDidLoad方法。但是如果直接在分类中重写,会导致基类代码中的viewDidLoad不执行。此时,我们就需要使用runtime相关的方法来解决该问题(具体方式见第4节,第2条方法交换,如果需要深入了解,可以看看动态埋点统计的实例)。


    1.3 常用runtime实现的强大功能

    OC本质上是C的扩展和封装。我们的OC代码运行时,底层调用的实际上是c语言的代码。runtime(翻译过来即运行时)就是苹果暴露给用户的一个偏底层的可以操作底层代码的API接口,是对常用的设计模式的一个必要补充。通过该接口的一些函数,我们可以直接干预消息发送过程,从而实现很多强大的功能。比如

    • (1) 实现多继承Multiple Inheritance (利用消息转发机制)
    • (2) 在分类中重写原类方法而又不失去原类方法中的功能 (利用class的Method Swizzling)
    • (3) Aspect Oriented Programming (切片编程)
    • (4) 重写class方法(Isa Swizzling)
    • (5) 给分类添加属性变量( 利用Associated Object给分类添加关联对象)
    • (6) 动态的增加方法 (利用消息转发机制,在运行时实现方法)
    • (7) NSCoding的自动归档和自动解档(利用类底层的结构查询函数,批量给所有属性自动添加相同的解档、归档方法)

    2. runtime API中主要内容

    2.1 对象、类的定义

    从下表可以看到,本质上类是一个指向类结构体的指针,而对象是一个指向对象结构体的指针,对象结构体中存储有一个isa类,它动态的指向该对象的类。类结构体中存储有类的名字,父类名字,类的成员变量(无论是通过@property还是直接定义的成员变量都存储在这里),类的实例变量大小(我们定义实例变量的时候变量空间大小就已经确定了),类的方法链表(普通类里面存储着该类的实例方法,元类中存储中该类的类方法),协议链表,方法缓存表(我们发送消息时第一个查询的结构体)等。

    //对象结构体中存储有一个isa类
    struct objc_object {
        Class _Nonnull isa  OBJC_ISA_AVAILABILITY;//Isa标识对象的类;它指向一个结构的类定义编译。
    };
    //所有的对象本质上都是一个id,而id是一个指向对象结构体的指针
    typedef struct objc_object *id;
    //类是一个指向类结构体的指针
    typedef struct objc_class *Class;` 
    `//类结构体中存储有该类定义的所有相关数据;` 
    `struct objc_class {`
    `Class isa  OBJC_ISA_AVAILABILITY;//Isa标识对象的类;它指向一个结构的类定义编译。`
    `#if !__OBJC2__`
    `Class super_class;``//父类`
    `const char *name;``//类名`
    `long version;``//类的版本信息,默认为0`
    `long info;``//类信息,供运行期使用的一些位标识`
    `long instance_size;``//类的实例变量大小`
    `struct objc_ivar_list *ivars;``// 类的成员变量链表`
    `struct objc_method_list **methodLists;``// 方法链表`
    `struct objc_cache *cache;``//方法缓存`
    `struct objc_protocol_list *protocols;``//协议链表#`
    `endif} `
    `OBJC2_UNAVAILABLE;`
    

    因为类也有一个isa 指针,所以类本质上也是一个对象,称为类对象。类对象Isa指针标识的类为该类的元类(meta class),每一个类都是这个元类的唯一实例对象。元类对象Isa指针标识的类为根元类,根元类(root meta Class)在整个系统中只有一个,所有的元类的isa指针都指向根元类,根元类的Isa指针标识的类为自己。具体如下所示,图中虚线代表类的isa指针指向,实线代表类的父类。根元类的父类是根类,同时根元类的实例对象也是根类(root class),这里形成了一个闭环。


    isa、superclass指针.png

    isa指针指向:实例对象->类->元类->(不经过父元类)直接到根元类(NSObject的元类),根元类的isa指向自己;

    2.2 Method、IMP、SEL的定义

    把他们拿出来说,是因为容易他们之间存在相关性和差异,非常容易产生误解,而且他们对我们理解消息机制很有帮助,我们可以看一下方法Method的定义如下:

    typedef struct objc_method *Method;
    struct objc_method {
        SEL _Nonnull method_name                                 OBJC2_UNAVAILABLE;
        char * _Nullable method_types                            OBJC2_UNAVAILABLE;
        IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;
    }       
    

    Method 是一个指向结构体的指针,它包含了IMP和SEL,还包含了方法类型定义、方法的参数等。
    SEL是方法的指针,但不同于C语言中的函数指针,函数指针直接保存了方法的地址,但SEL只是方法编号;
    IMP是方法的具体实现函数指针,在runtime里,我们可以使用函数改变或者设置IMP来更改一个函数的具体实现,例如:

    method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) //交换两个方法的实现
    method_setImplementation(Method _Nonnull m, IMP _Nonnull imp) // 给一个方法设置实现 
    
    2.3 runtime中常用的的一些函数

    runtime中的函数,一般按照结构体的层级结构来操纵。对类中成员进行操作的,以class开头,对方法中成员进行操作的以method开头,其他的以此类推。常见的函数如下:

    class_getProperty(Class _Nullable cls, const char * _Nonnull name)   //获取类的所有属性列表
    class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                    const char * _Nullable types)      //给类添加方法
    class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                        const char * _Nullable types)  //替换类方法
    class_addIvar(Class _Nullable cls, const char * _Nonnull name, size_t size, 
                  uint8_t alignment, const char * _Nullable types)    //增加类变量
    method_getImplementation(Method _Nonnull m)   //获取方法的实现
    method_setImplementation(Method _Nonnull m, IMP _Nonnull imp)  //设置方法的实现
    method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)  //交换方法的实现
    imp_implementationWithBlock(id _Nonnull block) //使用一个block创建一个实现
    sel_getName(SEL _Nonnull sel) //获取方法的名称
    sel_registerName(const char * _Nonnull str) //注册一个方法
    

    3.消息传递、转发机制

    想要合理的利用runtime中相关API接口,必须理解runtime中的消息传递、转发机制。
    (1)当一个对象发送消息时,首先,底层会执行一个消息发送函数,函数长这样

     objc_msgSend(void /* id self, SEL op, ... */ 
    

    如果是使用super发送消息,函数长这样:

     objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ 
    

    (2)底层会从该对象所属的类中(isa指针所指的类)的方法缓存列表查找对应的实现
    (3)如果2找不到,会从该类的方法链表中继续查找
    (4)如果3找不到,会跳转到该类的父类查找,父类步骤和子类一样,具体如下图所示。


    isa方法传递.gif

    (5)一直向上到根类,如果根类仍然找不到,就开始准备进行消息转发。转发第一步:动态消息解析。查看当前类是否实现了resolveInstanceMethod方法(如果是类方法,会看是否实现了resolveClassMethod方法)。如果该方法返回了YES,消息转发终止。我们可以在这个方法中动态添加方法实现,不实现也不要紧,只要返回YES消息发送就不会报错。

    +(BOOL)resolveClassMethod:(SEL)sel
    {
        NSString * selStr = NSStringFromSelector(sel);
        if ([selStr isEqualToString:@"runTest"]) {
    //注意,想要给类添加方法,必须添加到它的metaClass上,所以在class_addMethod中添加的类都要是原类!!!
    //  确定metaClass的方法是objc_getMetaClass(object_getClassName(self));
            if (class_addMethod(objc_getMetaClass(object_getClassName(self)), sel,class_getMethodImplementation(objc_getMetaClass(object_getClassName(self)), @selector(runTestFunction)), "s@:")) {
                return YES;
            }
            return [super resolveClassMethod:sel];
        }
        return [super resolveClassMethod:sel];
    }
    

    (6)如果第5步返回NO,就开始消息重定向。查看是否指定了其他对象来执行该方法。具体是查看当前类是否实现了forwardingTargetForSelector方法;如果该方法返回了一个对象,就在该对象上执行该selctor方法(该对象上执行该方法时步骤与本对象一致);

    -(id)forwardingTargetForSelector:(SEL)aSelector
    

    (7)如果第6步返回nil,就需要进行真正的消息转发机制。具体是查看当前类是否实现了methodSignatureForSelector方法,如果该方法返回不为nil,就执行forwardInvocation方法。如果forwardInvocation实现了,消息转发终止(但不见得消息转发完成,forwardInvocation只是一个消息的分发中心,将这些不能识别的消息转发给不同的接收对象,或者转发给同一个对象,再或者将消息翻译成另外的消息,亦或者简单的“吃掉”某些消息,因此没有响应也不会报错。)。

    - (void)forwardInvocation:(NSInvocation *)anInvocation
    {
        if ([someOtherObject respondsToSelector:
                [anInvocation selector]])
            [anInvocation invokeWithTarget:someOtherObject];
        else
            [super forwardInvocation:anInvocation];
    }
    

    (8)上述步骤,如果到第7部都没有实现,系统就会报错,提示unrecognized selector sent to instance
    注意:上述任何一步,都要在前一步骤没有完成的基础上 。

    消息转发机制.png

    4.如何使用runtime

    基于runtime提供的数据结构,以及上述消息传递、转发机制,runtime提供了丰富的函数让我们来实现我们第1节中提到的强大的功能,我们这里简单梳理下实现方式:

    • (1) 实现多继承Multiple Inheritance (利用消息转发机制),如下图所示。


      1330553-c7ef6392ecc9ee9d.gif

    我们在Warrior中头文件中定义一个方法negotiate,但是不实现它,而在forwardingTargetForSelector方法中,针对该selecotr,指定一个Diplomat对象,就可以将该方法实现交给diplomat类来实现。看起来就像是Warrior也继承了了Diplomat的方法一样(注意,像respondsToSelector:isKindOfClass: 这类方法只会考虑继承体系,不会考虑转发链。也就是说如果[Warrior respondsToSelector:negotiate]会返回NO)。

    • (2) 在分类中重写原类方法而又不失去原类方法中的功能 (利用class的Method Swizzling)

      实现方式是通过runtime中的实现交换函数method_exchangeImplementations。首先,在本类中定义另一个待交换的方法exchage_ViewDidLoad;待交换的方法中需要调用原方法,然后添加需要额外实现的功能(例如第1节中提到的数据统计方法)。在恰当的时机(一般是在load方法中),交换该两个方法的实现。实际执行代码的使用,调用原类方法会执行待交换的方法的实现,待交换的方法实现中又会调用原来的方法实现,从而保留了原来的方法的实现。

    +(void)exchangeOriginMethodWithMethodExchangeMethod
    {
    //    防止方法被多次调用后交换失效;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            SEL originSEL =  @selector(viewDidLoad);
            SEL swizzSEL = @selector(exchage_ViewDidLoad);
            Method viewDidLoad = class_getInstanceMethod([self class], originSEL);
            Method exchang_viewDidLoad = class_getInstanceMethod([self class], swizzSEL);
    //        测试原来的选择子是否已经添加了方法(是否已经交换了方法);
            Boolean didAddMethod = class_addMethod([self class], originSEL,method_getImplementation(exchang_viewDidLoad),method_getTypeEncoding(exchang_viewDidLoad));
            if (!didAddMethod) {
    //
                //        如果没有添加方法,就直接交换
                method_exchangeImplementations(viewDidLoad, exchang_viewDidLoad);
            }else{
    //            如果已经添加了,就同时更换交换后的方法实现;
                class_replaceMethod([self class], swizzSEL, method_getImplementation(viewDidLoad), method_getTypeEncoding(viewDidLoad));
            }
        });
    }
    -(void)exchage_ViewDidLoad
    {
        NSLog(@"%@ did load",self);
        [self exchage_ViewDidLoad];//注意,exchage_ViewDidLoad的实现现在是viewDidLoad了,所以没有循环调用
    }
    
    • (3) Aspect Oriented Programming (切片编程,内容太多,暂不展开,可以看这里)

    • (4) 重写class方法(Isa Swizzling)

      苹果著名的KVO技术和NSNotificationCenter就使用的该方法,在我们给一个对象添加了KVO键值观察方法后

    - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)contex。
    

    后台会重新创建一个NSKVONotifying_Object类,然后偷偷将原来的类的isa指针指向该类。该类中会在属性变量修改时候,调用

    -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
    

    方法,并发出相应的通知

    • (5) 给分类添加属性变量( 利用Associated Object给分类添加关联对象)

      我们可以给分类添加属性,但是分类不会自动给我们生成成员变量。因为类的成员变量在编译器已经决定了(写入了类的结构体中,具体见前面结构体的定义),但是category是在运行期才决议的。所以如果要给分类添加成员变量,需要用runtime里面函数在运行期实现。一般使用objc_setAssociatedObject和objc_getAssociatedObject函数来实现。这两个函数都是成对的出现,一个给对象添加关联对象,一个获取关联对象。具体代码如下。

      @property(nonatomic,strong)id associatedObjcet;
    -(void)setAssociatedObjcet:(id)associatedObjcet{
        objc_setAssociatedObject(self, @selector(associatedObjcet), associatedObjcet, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    -(id)associatedObjcet{
        return objc_getAssociatedObject(self, @selector(associatedObjcet));
    }
    
    • (6) 动态的增加方法 (利用消息转发机制,在运行时实现方法)

      动态的增加方法和多重继承有些类似,都是调用的方法在类中并没有实现代码,而是在消息转发机制的某一步才动态的添加实现代码。消息转发机制本身有多步骤,所以根据需要,可以在不同的步骤实现动态添加,常见的一般在方法动态解析resolveInstanceMethod或者在消息转发forwardInvocation的时候进行。

    • (7) NSCoding的自动归档和自动解档(利用类底层的结构查询函数,批量给所有属性自动添加相同的解档、归档方法)

    NSCoding其实就是对所有的属性调用encode和decode方法。使用手动操作有一个缺陷,如果属性多起来,要写好多行相似的代码,虽然功能是可以完美实现,但是看上去不是很优雅。用runtime实现的思路就比较简单,我们循环依次找到每个成员变量的名称,然后利用KVC读取和赋值就可以完成encodeWithCoder和initWithCoder了,部分代码如下:

     Ivar *vars = class_copyIvarList([self class], &outCount); 
    for (int i = 0; i < outCount; i ++) {
     Ivar var = vars[i]; 
    const char *name = ivar_getName(var); 
    NSString *key = [NSString stringWithUTF8String:name];
     id value = [aDecoder decodeObjectForKey:key];
     [self setValue:value forKey:key]; 
     }
    

    相关文章

      网友评论

          本文标题:iOS runtime 理解消息传递、转发机制和使用案例

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