一、初识Runtime数据结构

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数据结构)