OC知识梳理:Runtime

1,250 阅读6分钟

数据结构

1. objc_object

OC中平时使用的所有的对象(id类型),在Runtime中对应为objc_object的结构体。

objc_object结构体:

  • isa_t
  • 关于isa操作相关
  • 弱引用相关
  • 关联对象相关
  • 内存管理相关

2. objc_class

OC中所使用的类(Class),在Runtime中对应为objc_class。继承自objc_object。 Class也是一个对象,称之为类对象。

objc_class结构体:

  • Class superClass;指向父类的指针
  • cache_t cache;方法缓存,进行消息传递过程中会使用到
  • class_data_bits_t bits;类所定义的协议、属性以及方法

3. isa指针

  • 指针型isa:isa的值代表Class的地址。
  • 非指针型isa:isa的值得部分代表Class的地址。

在使用指针寻址过程中,例如在64位设备上,也许30、40位就能保证寻找到所有Class的地址,那么非指针型isa可以存储一些其他内容以达到节省内存的目的。

isa指向

  • 关于对象,其指向类对象。
  • 关于类对象,其指向元类对象。
  • 所有元类对象的isa指针都指向根元类对象,包括根元类对象自己。
graph LR
A[实例] -->|isa| B[Class]
B -->|isa| C[MetaClass]

4. cache_t

对象调用方法都是通过编译器进行方法查找。而编译器会将经常查找的方法进行缓存,下次查找的时候先进入缓存中查找,这样会大大节省时间,从而提高查找速度,cache_t就是为此而生的。

  • 用于快速查找方法执行函数
  • 是可增量扩展的哈希表结构
  • 是局部性原理的最佳应用

可以理解为是装满了bucket_t结构体的Hash表。bucket_t包含两个主要的成员变量:key、IMP。其中的Key对应OC中的SEL,就是一个字符串,表示方法的名字;IMP则是指向方法实现首地址的指针。

5. class_data_bits_t

  • class_data_bits_t主要是对class_rw_t的封装。
  • class_rw_t代表了类相关的读写信息,是对class_ro_t的封装。
  • class_ro_t代表了类相关的只读信息。

class_rw_t/class_ro_t中的rw/ro代表readwrite/readonly

6. class_rw_t

  • class_ro_t
  • properties:属性
  • protocols:协议
  • methods:方法列表(一般为分类添加的方法)

class_rw_t结构体中的properties、protocols、methods都是继承自list_array_tt的二维数组。

7. class_ro_t

  • name:类名
  • ivars:成员变量
  • properties:属性
  • protocols:协议
  • methodList:方法列表(原始定义的方法)

class_ro_t结构体中的ivars、properties、protocols、methodList都是一维数组。

8. method_t

  • SEL name;函数名称
  • const char* types;返回值和参数(Type Encodings)
  • IMP imp;指向函数实现的首地址指针

函数四要素:名称、返回值、参数、函数体。

消息传递

1. 对象、类对象、元类对象

实例对象是objc_object数据结构; 类对象和元类对象,都是属于objc_class数据结构,而objc_class数据结构又是继承自objc_object数据结构。 实例对象可以通过isa指针找到它的类对象,类对象存储实例方法列表等信息; 类对象可以通过isa指针找到它的元类对象,元类对象存储类方法列表等信息; 根类对象的superClass指针指向nil; 所有元类对象的isa指针,都指向根元类对象,包括根元类自己; 根元类对象的superClass指针指向根类对象;

2. 实例方法的消息传递

  • 当我们调用实例方法时,系统会先根据当前实例的isa指针找到它的类对象;
  • 在类对象中先查找缓存,再遍历方法列表查找同名的方法实现;
  • 如果没有查找到,则会按照类对象的superClass指向,依次遍历父类的方法列表,直到根类对象;
  • 如果一直到根类对象也没有查找到该方法,则进入消息转发流程。

3. 类方法的消息传递

  • 当我们调用类方法时,系统会通过类对象的isa指针找到它的元类对象;
  • 在元类对象中先查找缓存,再遍历方法列表查找同名的方法实现;
  • 如果没有查找到,则会按照元类对象的superClass指向,依次遍历父元类的方法列表,直到根元类对象;
  • 如果一直到根元类对象也没有查找到该方法,因为根元类的superClass指向根类对象,则会在根类对象中找同名的实例方法实现;
  • 如果根类对象中有同名的实例方法,则会执行该实例方法;
  • 如果根类对象中没有同名的实例方法,则进入消息转发流程。

4. [self class]、[super class]

  • [self class] ==> objc_msgSend(self, @selector(class))
  • [super class] ==> objc_msgSendSuper(super, @selector(class))

虽然在调用super的方法时,runtime中传递的参数是super,但这里的super其实是一个objc_super类型的结构体,该结构体中包含了一个receiver对象,指的是当前对象,而不是其父类对象。所以[self class]和[super class]的返回都是当前对象的class,区别是super调用时,跳过了在当前类对象的方法列表遍历@selector(class)的过程,直接从父类的方法列表开始查。

5. 缓存查找cache_t

根据给定的方法选择器(SEL),通过哈希查找,来映射出要找的bucket_t在数组中的位置,最后取得该方法的实现(IMP)。

哈希查找:通过给定的Key,经过哈希函数的算法算出一个值,算出的值就是这个Key在数组中对应元素的索引位置。

6. 当前类中查找

  • 对于已排序好的列表,采用二分查找算法查找;
  • 对于没有排序的列表,采用一般遍历方法查找。

7. 消息转发流程

  1. resolveInstanceMethod:/resolveClassMethod:首先系统会回调该类方法,Instance表示实例方法的消息转发、Class表示类方法的消息转发。参数是方法选择器(SEL),返回值是BOOL类型,表示系统要不要解决该方法实现。

  2. forwardingTargetForSeLector:当第1步的方法返回NO时,系统会回调该方法。参数也是方法选择器(SEL),返回值是id,表示这条消息应该由哪个对象来处理。

  3. methodSignatureForSelector:若第2步的方法没有指定转发目标,系统会调用该方法,也是消息转发的最后一次机会。参数是方法选择器(SEL),返回值是methodSignature类型的对象,封装了方法选择器返回值的类型以及参数个数和参数类型。此时若返回了方法签名,系统会调用forwardInvocation:方法;若forwardInvocation:无法处理这条消息,或methodSignatureForSelector:返回了nil,会导致Crash。

graph LR
A[resolveInstanceMethod:] -->|返回NO| B[forwardingTargetForSeLector:]
A -->|返回YES| F[消息已处理]
B -->|返回nil| C[methodSignatureForSelector:]
B -->|返回转发目标| F
C -->|返回方法签名| D[forwardInvocation:]
C -->|返回nil| E[消息无法处理]
D --> E
D --> F

8. 动态方法解析

  • 动态运行时语言将函数决议推迟到运行时。
  • 编译时语言在编译期进行函数决议。

当我们把一个属性标识为@dynamic时,代表着不需要编译器在编译时为我们生成该属性get/set方法的具体实现,而是在运行时我们具体的调用了该属性的get/set方法时再去添加具体的实现。

9. 能否向编译后的类中增加实例变量?

不能。类在编译之前,已经完成了实例变量的布局,存放于class_ro_t,是只读的。

10. 能否向动态添加的类中增加实例变量?

可以。在动态添加类的过程中,只要在注册之前,就可以添加实例变量。