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_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(类的变量、属性、方法)
如图所示:
三、isa指针
- isa 指针的含义?
在C++中是一个共用体,被定义成了isa_t.它实际是32或64位0或者1的数字。它分为指针型的isa(整体内容代表Class的地址)以及非指针型的isa(部分内容代表Class的地址,为了节约内存开销)。 - isa 指针的指向
- 对于对象,其指向类对象。
OC中实例对象是id 类型,在runtime中就是objc_object类型其含有一个isa,该isa会指向父类对象。 - 对于类对象,其指向元类对象.
Class-----isa----->MetaClass
因为Class是类对象,因为它继承于objc_object。也拥有一个isa,而它的isa指针是指向元类对象的。
3.方法的调用过程
- 如果调用的是实例方法时,实际上是实例的isa指针到它的类对象的方法中查找,
- 如果调用的是类方法,类对象的isa 指针会到它的元类对象的方法中进行查找.
四、cache_t cache
- cache 指针的含义?
- 用于快速查找方法执行函数。 (也就是说,当调用方法时,如果缓存中有,那么就不会到它的方法列表中查找。)
- 是可增量扩展的哈希表结构。(可增量扩展体现在:当我们的结构,存储量增大时,他也会扩大自己的内存空间,来存储。使用哈希表存储,主要是提高查找效率。)
- 局部性原理的最佳应用。(局部性原理,在我们使用时,也许就那么几个方法,如果将其存储下来,下次的在调用的时候命中率更高一些,从而提高程序的运行速度。)
- 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 代表了类相关的只读信息
- class_rw_t 主要包含有class_ro_t\protocols\properties\methods.
而protocols\properties\methods时间是一个继承于list_array_tt 的二维数组。methods数组中的元素是一个方法列表,子数组中的元素都是方法列表中的method_t 数据结构。如图所示:
- 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.
他们分别对应函数四要素是:函数名、参数与返回值、函数体。
如下图所示:
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)整体的数据结构如何组合在一起的??
如下图所示:
七、对象、类对象、元类对象
- 类对象是存储实例方法列表信息
- 元类对象是存储类的类方法列表信息
- 消息传递流程:
使用---> 表示 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中查找,如果查找到,就调用,就完成了一次消息传递。如果在缓存中没有查找到,就会到当前类的方法列表中的查找,如果还是没有找到就到其父类(逐级父类)查找,直到根类对象,如果还没有查到,就进行消息转发。
- 缓存查找流程(哈希查找)
例如:给定的SEL ,查找对应bucket_t 的 IMP(方法实现).
详细说:就是根据给定的SEL,通过缓存策略或者函数,查找到对应的映射出bucket_t在数组中的位置, 然后,提取出对应的IMP方法实现。通过给定的值,经过哈希算法的 f(key) = key & mask,
这个f(key) 就是bucket_t 在数组中的索引位置。
- 当前类中的查找
- 对于已经排序好的列表,采用二分法查找算法查找方法对应的执行函数
- 对于没有排好序的列表,采用一般遍历查找方法对应执行函数。
-
父类逐级查找
如图所示:
首先根据当前类的superclass 指针,找到对应的父类,然后在父类的缓存列表中查找如果查找到了,就结束本次的父类逐级查找,如果没有找到就去遍历父类的方法列表。如果查找到了,就返回给调用方,如果没有查找到,就到父类的父类,直到到NSObject中,如果还未找到就返回nil.
八、消息转发流程(实例方法)
对于实例方法的转发:首先系统会回调resolveInstanceMethod:---参数:SEL 返回值:BOOL,要不要解决当前方法的实现。类方法----->返回是YES,当前消息已经处理,如果为NO,系统会给我我们第二次处理这条消息,会调用(id)forwardingTargetForSelector:告诉系统这个消息由谁处理,转发对象是谁。如果我们给定了一个转发目标的话,系统将这条消息返回给我们指定的转发对象,同时结束当前的消息转发。如果没有给出转发目标(返回nil),系统会再次给我们处理这条消息的机会,调用methodSignatureForSelector: 返回值是对象,它对返回值类型以及参数个数的封装,此时如果我们返回了方法签名,系统会调用forwardInvocation:如果能处理这条消息,则转发流程结束。如果methodSignatureForSelector返回nil或者forwardInvocation,被标记为消息无法处理。
具体流程印证,可参照RuntimeObject
九、 Method Swizzling 方法混淆
具体使用方法,可参照RuntimeObject
十、动态添加方法
十一、动态方法解析
- @dynamic
它所修饰的属性的setter、getter方法是在运行时添加的。
- 动态运行时语言将函数决议推迟到了运行时。
就是说在运行时,我们调用属性的setter、getter方法的时候再回创建setter、getter方法。 - 编译时语音在编译期进行函数决议。
就是说,在编译的时候,就确定了一个函数名的对应的实现体,我们时不能进行修改的。