1. 什么是 Runtime?
我们都知道,将源代码转换为可执行的程序,通常要经过三个步骤:编译、链、运行。不同的编译语言,在这三个步骤中所进行的操作又有些不同。
C 语言作为一门静态类语言,在编译阶段就已经确定了所有变量的数据类型,同时也确定好了要调用的函数,以及函数的实现。
而Objective-C 语言是一门动态语言。在编译阶段并不知道变量的具体数据类型,也不知道所真正调用的哪个函数。只有在运行时间才检查变量的数据类型,同时在运行时才会根据函数名查找要调用的具体函数。
Objective-C 语言把一些决定性的工作从编译阶段、链接阶段推迟到运行时阶段的机制的基础就是Runtime。Runtime实际上是一个库,这个库使我们可以在程序运行时动态的创建对象、检查对象。
2. 消息机制的基本原理
Objective-C 语言中,对象方法调用都是类似[receiver selector];的形式,其本质就是让对象在运行时发送消息的过程。
我们来看看方法调用[receiver selector];在『编译阶段』和『运行阶段』分别做了什么?
编译阶段:[receiver selector];方法被编译器转换为:
objc_msgSend(receiver,selector)(不带参数)
objc_msgSend(recevier,selector,org1,org2,…)(带参数)
运行时阶段:消息接受者recevier寻找对应的selector。
通过recevier的isa 指针找到recevier的Class(类);
在Class(类)的cache(方法缓存)的散列表中寻找对应的IMP(方法实现);
如果在cache(方法缓存)中没有找到对应的IMP(方法实现)的话,就继续在Class(类)的method list(方法列表)中找对应的selector,如果找到,填充到cache(方法缓存)中,并返回selector;
如果在Class(类)中没有找到这个selector,就继续在它的superClass(父类)中寻找;
一旦找到对应的selector,直接执行recevier对应selector方法实现的IMP(方法实现)。
若找不到对应的selector,消息被转发或者临时向recevier添加这个selector对应的实现方法,否则就会发生崩溃。
在上述过程中涉及了好几个新的概念:objc_msgSend、isa 指针、Class(类)、IMP(方法实现)等,下面我们来具体讲解一下各个概念的含义。
3. Runtime 中的概念解析
3.1 objc_msgSend
所有 Objective-C 方法调用在编译时都会转化为对 C 函数 objc_msgSend 的调用。objc_msgSend(receiver,selector); 是 [receiver selector]; 对应的 C 函数。
3.2 Class(类)
在 objc/runtime.h 中,Class(类) 被定义为指向 objc_class 结构体 的指针,objc_class 结构体 的数据结构如下:
struct objc_class {
Class _Nonnull isa; // objc_class 结构体的实例指针
#if !__OBJC2__
Class _Nullable super_class; // 指向父类的指针
const char * _Nonnull name; // 类的名字
long version; // 类的版本信息,默认为 0
long info; // 类的信息,供运行期使用的一些位标识
long instance_size; // 该类的实例变量大小;
struct objc_ivar_list * _Nullable ivars; // 该类的实例变量列表
struct objc_method_list * _Nullable * _Nullable methodLists; // 方法定义的列表
struct objc_cache * _Nonnull cache; // 方法缓存
struct objc_protocol_list * _Nullable protocols; // 遵守的协议列表
#endif
};
从中可以看出,objc_class 结构体 定义了很多变量:自身的所有实例变量(ivars)、所有方法定义(methodLists)、遵守的协议列表(protocols)等。objc_class 结构体 存放的数据称为 元数据(metadata)。
objc_class 结构体 的第一个成员变量是 isa 指针,isa 指针 保存的是所属类的结构体的实例的指针,这里保存的就是 objc_class 结构体的实例指针,而实例换个名字就是 对象。换句话说,Class(类) 的本质其实就是一个对象,我们称之为 类对象。
3.3 Object(对象)
接下来,我们再来看看 objc/objc.h 中关于 Object(对象) 的定义。
Object(对象)被定义为 objc_object 结构体,其数据结构如下:
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa; // objc_object 结构体的实例指针
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;
这里的 id 被定义为一个指向 objc_object 结构体 的指针。从中可以看出 objc_object 结构体 只包含一个 Class 类型的 isa 指针。
换句话说,一个 Object(对象)唯一保存的就是它所属 Class(类) 的地址。当我们对一个对象,进行方法调用时,比如 [receiver selector];,它会通过 objc_object 结构体的 isa 指针 去找对应的 object_class 结构体,然后在 object_class 结构体 的 methodLists(方法列表) 中找到我们调用的方法,然后执行。
3.4 Meta Class(元类)
从上边我们看出,对象(objc_object 结构体) 的 isa 指针 指向的是对应的 类对象(object_class 结构体)。那么 类对象(object_class 结构体)的 isa 指针 又指向什么呢?
object_class 结构体 的 isa 指针 实际上指向的的是 类对象 自身的 Meta Class(元类)。
那么什么是 Meta Class(元类)?
Meta Class(元类) 就是一个类对象所属的 类。一个对象所属的类叫做 类对象,而一个类对象所属的类就叫做 元类。
Runtime 中把类对象所属类型就叫做 Meta Class(元类),用于描述类对象本身所具有的特征,而在元类的 methodLists 中,保存了类的方法链表,即所谓的「类方法」。并且类对象中的 isa 指针 指向的就是元类。每个类对象有且仅有一个与之相关的元类。
在 2. 消息机制的基本原理 中我们讲解了 对象方法的调用过程:
1、通过对象的 isa 指针 找到 对应的 Class(类);
2、然后在 Class(类) 的 method list(方法列表) 中找对应的 selector 。
而 类方法的调用过程 和对象方法调用差不多,流程如下:
1、 通过类对象 isa 指针 找到所属的 Meta Class(元类);
2、在 Meta Class(元类) 的 method list(方法列表) 中找到对应的 selector;
3、执行对应的 selector。
3.5 实例对象、类、元类之间的关系
我们先来看 isa 指针:
水平方向上,每一级中的 实例对象 的 isa 指针 指向了对应的 类对象,而 类对象 的 isa 指针 指向了对应的 元类。而所有元类的 isa 指针 最终指向了 NSObject 元类,因此 NSObject 元类 也被称为 根元类。
垂直方向上, 元类 的 isa 指针 和 父类元类 的 isa 指针 都指向了 根元类。而 根元类 的 isa 指针 又指向了自己。
我们再来看 父类指针:
类对象 的 父类指针 指向了 父类的类对象,父类的类对象 又指向了 根类的类对象,根类的类对象 最终指向了 nil。
元类 的 父类指针 指向了 父类对象的元类。父类对象的元类 的 父类指针指向了 根类对象的元类,也就是 根元类。而 根元类 的 父亲指针 指向了 根类对象,最终指向了 nil。