前言
Runtime 是一套底层的 C 语言 API,是 iOS 系统的核心之一。开发者在编码过程中,可以给任意一个对象发送消息,在编译阶段只是确定了要向接收者发送这条消息,而接受者将要如何响应和处理这条消息,那就要看运行时来决定了。
C语言中,在编译期,函数的调用就会决定调用哪个函数。而OC的函数,属于动态调用过程,在编译期并不能决定真正调用哪个函数,只有在真正运行时才会根据函数的名称找到对应的函数来调用。
Objective-C 是一个动态语言,这意味着它不仅需要一个编译器,也需要一个运行时系统来动态得创建类和对象、进行消息传递和转发。
使用Runtime
Objc 在三种层面上与 Runtime 系统进行交互:
1. 通过 Objective-C 源代码
一般情况开发者只需要编写 OC 代码即可,Runtime 系统自动在幕后把我们写的源代码在编译阶段转换成运行时代码,在运行时确定对应的数据结构和调用具体哪个方法。
2. 通过 Foundation 框架的 NSObject 类定义的方法
在Foundation框架下,NSObject和NSProxy两个基类定义了类层次结构中该类下方所有类的公共接口和行为。
在iOS 11.2 的 objc/NSObject.h 中,与Runtime相关的方法有5个:
1 | @protocol NSObject |
在NSObject.h中还有一个方法会返回指定方法实现的地址IMP
1 | @interface NSObject <NSObject> |
3. 通过对 Runtime 库函数的直接调用
objc_class
本文所有源码来自 objc4-680
在objc.h中定义了Class
1 | /// An opaque type that represents an Objective-C class. |
objc_class定义在runtime.h中,Objc 2.0之前 objc_class 定义如下:
1 | struct objc_class { |
methodLists是指向方法列表的指针。这里可以动态修改methodLists的值来添加成员方法,这也是Category实现的原理,同样解释了Category不能添加属性的原因。
Objc 2.0之后 objc_class 定义如下:
1 | struct objc_object { |
其中 isa 是一个联合体,定义如下:1
2
3
4
5
6
7union isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
}
cache_t 的作用主要是为了优化方法调用的性能。其结构如下:1
2
3
4
5struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
}
在NSObject中,isa 是一个 objc_class 结构体,id 是一个 objc_object 结构:1
2
3
4
5
6
7
8
9
10
11
12typedef struct objc_class *Class;
typedef struct objc_object *id;
@interface Object {
Class isa;
}
@end
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
@end
用一张图来更加详细看到 objc_class 的结构:

objc_class继承于objc_object,在objc_class中也包含isa_t类型的结构体isa。从代码中可以看出来:Objective-C 中类也是一个对象。
IMP是一个函数指针,指向了一个方法的具体实现。
现在,我们再仔细看一下 objc_class 的结构,除了 isa 之外,还有3个成员变量,一个是父类的指针 superclass ,一个是方法缓存 cache_t ,最后一个这个类的实例方法链表 class_data_bits_t 。1
2
3
4
5
6struct objc_class : objc_object {
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
...
};
superclass 和 isa
当一个对象的实例方法被调用的时候,会通过 isa 找到相应的类,然后在该类的 class_data_bits_t 中去查找方法。 class_data_bits_t 是指向了类对象的数据区域。
为了和对象查找方法的机制一致,引入了元类(meta-class)的概念在类对象中查找。
- 对象的实例方法调用时,通过对象的 isa 在类中获取方法的实现。
- 类对象的类方法调用时,通过类的 isa 在元类中获取方法的实现。
对象,类,元类之间的关系可以表示为:
Root class (class)其实就是NSObject,NSObject是没有超类的,所以Root class(class)的superclass指向nil。- 每个
Class都有一个isa指针指向唯一的Meta class Root class(meta)的superclass指向Root class(class),也就是NSObject,形成一个回路。- 每个
Meta class的isa指针都指向Root class (meta)。
类对象和元类对象是唯一的,而对象是可以在运行时创建无数个的。在main方法执行之前,从 dyld 到 runtime 这期间,类对象和元类对象在这期间被创建。
cache_t
Cache的作用主要是为了优化方法调用的性能。使用Cache来缓存经常调用的方法,当调用方法时,优先在Cache查找,如果没有找到,再到methodLists查找。
1 | struct cache_t { |

cache_t 中存储了一个 bucket_t 的结构体,和两个 unsigned int 的变量。
mask:分配用来缓存bucket的总数。occupied:表明目前实际占用的缓存bucket的个数。
bucket_t 的结构体中存储了一个 unsigned long 和一个 IMP 。IMP是一个函数指针,指向了一个方法的具体实现。
cache_t 中的 bucket_t *_buckets 其实就是一个散列表,用来存储Method的链表。
class_data_bits_t

在 objc_class 结构体中的注释写到 class_data_bits_t 相当于 class_rw_t 指针加上 rr/alloc 的标志。
Objc的类的属性、方法、以及遵循的协议在obj 2.0的版本之后都放在class_rw_t中。class_ro_t是一个指向常量的指针,存储来编译器决定了的属性、方法和遵守协议。
在编译期类的结构中的 class_data_bits_t *data指向的是一个 class_ro_t *指针:

在运行时调用 realizeClass 方法,会做以下3件事情:
- 从
class_data_bits_t调用data方法,将结果从class_rw_t强制转换为class_ro_t指针 - 初始化一个
class_rw_t结构体 - 设置结构体
ro的值以及flag
最后调用methodizeClass方法,把类里面的属性,协议,方法都加载进来。
1 | struct method_t { |
方法method的定义如上,里面包含3个成员变量。SEL是方法的名字name。types是Type Encoding类型编码,IMP是一个函数指针,指向的是函数的具体实现。在runtime中消息传递和转发的目的就是为了找到IMP,并执行函数。
总结
最后用一张图来总结整个运行时的过程:
