美文网首页
一、初识Runtime数据结构

一、初识Runtime数据结构

作者: 荷码人生 | 来源:发表于2019-09-29 09:33 被阅读0次

    Runtime的数据结构主要包括objc_object、objc_class、isa 指针、method_t对象。下面我们一起学习一下吧。

    一、objc_object

    我们实际中使用的都是id类型的对象,id 类型的对象在runtime 中对应的就是objc_object结构体,它主要包括一下几个成员变量:
    (1)isa_t (共用体)
    (2)关于isa的相关操作通过objc_object结构体获取它的isa类对象,以及通过类对象的isa获取元类对象的实例以及方法。
    (3)弱引用相关的方法(标记一个对象是否曾经有过弱引用指针)
    (4)关于关联对象相关的方法。(为对象设置了关联属性,而这些关联属性的方法也体现在objc_object结构体中)
    (5)内存管理方法的实现(retain\release@autoReleasePool)

    如下图所示:

    objc_object结构示意图

    二、objc_class

    我们在OC语言中所使用到的Class对应于runtime中objc_class结构体。objc_class继承与objc_object. objc_class包含的成员变量是:
    (1)Class superclass指针,它指向对象也是Class类型,如果说是类对象那么它指向元类对象。如果是实例对象它指向父类对象。
    (2)cache_t cache 它表达了方法缓存的结构。
    (3)class_data_bits bits(类的变量、属性、方法)

    如图所示:


    objc_class结构示意图

    三、isa指针

    1. isa 指针的含义?
      在C++中是一个共用体,被定义成了isa_t.它实际是32或64位0或者1的数字。它分为指针型的isa(整体内容代表Class的地址)以及非指针型的isa(部分内容代表Class的地址,为了节约内存开销)。
    2. isa 指针的指向
    • 对于对象,其指向类对象。
      OC中实例对象是id 类型,在runtime中就是objc_object类型其含有一个isa,该isa会指向父类对象。
    • 对于类对象,其指向元类对象.
      Class-----isa----->MetaClass
      因为Class是类对象,因为它继承于objc_object。也拥有一个isa,而它的isa指针是指向元类对象的。
    isa指针指向

    3.方法的调用过程

    • 如果调用的是实例方法时,实际上是实例的isa指针到它的类对象的方法中查找,
    • 如果调用的是类方法,类对象的isa 指针会到它的元类对象的方法中进行查找.

    四、cache_t cache

    1. cache 指针的含义?
    • 用于快速查找方法执行函数。 (也就是说,当调用方法时,如果缓存中有,那么就不会到它的方法列表中查找。)
    1. 是可增量扩展的哈希表结构。(可增量扩展体现在:当我们的结构,存储量增大时,他也会扩大自己的内存空间,来存储。使用哈希表存储,主要是提高查找效率。)
    2. 局部性原理的最佳应用。(局部性原理,在我们使用时,也许就那么几个方法,如果将其存储下来,下次的在调用的时候命中率更高一些,从而提高程序的运行速度。)
    3. cache_t 的数据机构
      可以看作是一个包含若干个bucket_t 结构体的数组。bucket_t 主要包含两个成员变量:key与IMP(key对应OC 中的selector,IMP是对应的函数指针,或者是函数体). 如果有一个Key,可以通过哈希算法,定位到相应的bucket_t,然后从bucket_t中提取到IMP的具体函数实现,从而完成方法的调用。

    五、class_data_bits bits(类的变量、属性、方法)

    • class_data_bits_t主要是对class_rw_t的封装。
    • class_rw_t 代表了类相关的读写信息、对class_ro_t的封装。
    • class_ro_t 代表了类相关的只读信息
    1. class_rw_t 主要包含有class_ro_t\protocols\properties\methods.
      而protocols\properties\methods时间是一个继承于list_array_tt 的二维数组。methods数组中的元素是一个方法列表,子数组中的元素都是方法列表中的method_t 数据结构。如图所示:
    class_rw_t结构图
    1. class_ro_t 数据结构
      包括:name/ivars/protocols/properties/methodList.
      而ivars/protocols/properties/methodList 是一维数组。methodList的元素:method_t.

    六、method_t方法列表

    首先,我们一起回顾一下有关函数的知识。
    函数的四要素:函数声明名、参数、返回值、函数体。
    method_t 是函数结构体,是对函数四要素的封装。
    method_t 中的成员有:SEL name、const char * types、无类型的IMP imp.
    他们分别对应函数四要素是:函数名、参数与返回值、函数体。
    如下图所示:


    方法结构体.png

    struct method_t 返回值和参数如何表示的?涉及到了Type Encodings技术,使用到了const char * types的字符指针,该指针的数据结构,是这样的,
    首位并且仅有一位,是返回值,接着是objc_messageSned的两个固定参数,最后是,我们自定的参数。比如说:
    -(void)aMessage;
    它的const char * types 就可以表示为:v@: 其中 v 代表 返回值为void类型、@ 代表 id 类型 、 : 代表选择器SEL.

    我们所调用的方法或者消息传递到达runtime时,都会转化成objc_messageSend:这样的函数调用。而@和: 就是这个函数的两个固定的参数,并且第一个参数必须是id类型的对象(消息的接收者)。

    (4)整体的数据结构如何组合在一起的??

    如下图所示:


    Runtime的数据结构.png

    七、对象、类对象、元类对象

    • 类对象是存储实例方法列表信息
    • 元类对象是存储类的类方法列表信息
    消息传递流程.png
    1. 消息传递流程:

    使用---> 表示 isa ,使用 ====> 表示superclass

    根类的类对象对应于OC 中NSObject,它的父类是nil,也就是没有父类。它的子类为superclass以及子类的子类subclass.首先我们有一个实例变量(instance of Subclass)----> Subclass类----> Meta of Subclass

    三者之间的关系:实例对象可以通过isa指针找到自己的类对象,而父类的类对象又可以通过isa 指针找到自己的元类对象。

    对于任何一个元类对象的isa 指针,都指向根元类对象,包括根元类对象的isa 指针也指向它本身

    对于它的superclass 指针指向传递,如下所述:

    类对像之间的指向:
    Subclass(class) ===> Superclass(class) ===>Rootclass(class) ===> nil

    元类对象之间的指向:
    Subclass(meta) ===> Superclass(meta) ===>Rootclass(meta) ===>Rootclass(class) ===> nil

    注意:根元类对象的superclass指针指向根类对象,最后指向nil 。也就是说,当我们调用类方法的时候,我们会沿着上面的路径查找,最终会到根元类对象的类方法列表中查找,如果查找不到,这时再去根类对象的实例方法列表中查找同名的实例方法,仍然没有查到的话,就直接返回nil抛出异常(常见的:unrecognized selector sent to class 0x10cc4ace0')

    消息传递流程的具体描述

    • 调用实例方法
      当我们调用实例方法时,首先实例对象会根据自己的isa指针,找到对应的类对象,在该类对象的实例方法列表中遍历查找同名的方法实现,如果没有查到,类对象再根据自己的superclass指针找到其父类对象,再在父类对象的实例方法列表,遍历查找同名的方法实现,还是没有找到的话,父类对象再根据superclass指针找到其根类对象,在进行查找。如果还没有找到,就进行消息转发流程。

    • 调用类方法
      首先,类对象会根据isa指针找到其元类对象,在元类对象的类方法列表中遍历查找同名方法,如果没有查到,就会根据其superclass 指针,到父元类,在到根源类-->根类-->最后到nil.

    我们来看一下这样一个面试题

    面试问题

    输出结果是什么? 全部都是Phone .
    具体分析:

    • [self class] 消息的接收者:当前类对象
    • [super class] 消息的接收者:当前对象??

    两者的消息的接收对象都是当前对象,只是super class 是从当前对象的父类对象的方法列表中,查找,最终都会到NSObject中查到class方法的实现。

    消息传递在编译过后都会转化成相应的方法调用:

    void objc_msgSend(void/id self ,SEL op,.../);

    [self class] ----> objc_msgSend(self,@selector(class));

    void objc_msgSendSuper(void/struct objc_super * super ,SEL op,.../);super是编译器关键字,编译器编译后,会将super解析成objc_super结构体指针,该结构体的成员变量就是receiver,receiver就是当前对象。

    struct objc_super{ _unsafe_unretained id receiver;}
    [super class] ----> objc_msgSendSuper(self,@selector(class));

    消息传递的流程图:


    消息传递的流程图

    描述:我们调用方法的流程
    首先,会从方法缓存cache_t中查找,如果查找到,就调用,就完成了一次消息传递。如果在缓存中没有查找到,就会到当前类的方法列表中的查找,如果还是没有找到就到其父类(逐级父类)查找,直到根类对象,如果还没有查到,就进行消息转发。

    1. 缓存查找流程(哈希查找)

    例如:给定的SEL ,查找对应bucket_t 的 IMP(方法实现).

    详细说:就是根据给定的SEL,通过缓存策略或者函数,查找到对应的映射出bucket_t在数组中的位置, 然后,提取出对应的IMP方法实现。通过给定的值,经过哈希算法的 f(key) = key & mask,
    这个f(key) 就是bucket_t 在数组中的索引位置。


    哈希查找流程.png
    1. 当前类中的查找
    • 对于已经排序好的列表,采用二分法查找算法查找方法对应的执行函数
    • 对于没有排好序的列表,采用一般遍历查找方法对应执行函数。
    1. 父类逐级查找
      如图所示:


      父类逐级查找

    首先根据当前类的superclass 指针,找到对应的父类,然后在父类的缓存列表中查找如果查找到了,就结束本次的父类逐级查找,如果没有找到就去遍历父类的方法列表。如果查找到了,就返回给调用方,如果没有查找到,就到父类的父类,直到到NSObject中,如果还未找到就返回nil.

    八、消息转发流程(实例方法)

    对于实例方法的转发:首先系统会回调resolveInstanceMethod:---参数:SEL 返回值:BOOL,要不要解决当前方法的实现。类方法----->返回是YES,当前消息已经处理,如果为NO,系统会给我我们第二次处理这条消息,会调用(id)forwardingTargetForSelector:告诉系统这个消息由谁处理,转发对象是谁。如果我们给定了一个转发目标的话,系统将这条消息返回给我们指定的转发对象,同时结束当前的消息转发。如果没有给出转发目标(返回nil),系统会再次给我们处理这条消息的机会,调用methodSignatureForSelector: 返回值是对象,它对返回值类型以及参数个数的封装,此时如果我们返回了方法签名,系统会调用forwardInvocation:如果能处理这条消息,则转发流程结束。如果methodSignatureForSelector返回nil或者forwardInvocation,被标记为消息无法处理。

    实例方法消息转发流程 类方法消息转发流程.png

    具体流程印证,可参照RuntimeObject

    九、 Method Swizzling 方法混淆

    具体使用方法,可参照RuntimeObject

    十、动态添加方法

    十一、动态方法解析

    1. @dynamic
      它所修饰的属性的setter、getter方法是在运行时添加的。
    • 动态运行时语言将函数决议推迟到了运行时。
      就是说在运行时,我们调用属性的setter、getter方法的时候再回创建setter、getter方法。
    • 编译时语音在编译期进行函数决议。
      就是说,在编译的时候,就确定了一个函数名的对应的实现体,我们时不能进行修改的。

    十二、Runtime实战

    面试题总结

    相关文章

      网友评论

          本文标题:一、初识Runtime数据结构

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