RunTime一直是iOS开发中非常重要的而且必须要理解的东西,最近在学习RunTime,有自己的一些心得,现在记录下来,便于以后查阅
理解 Objective-C 的 Runtime 机制可以帮我们更好的了解OC语言,适当的时候还能对语言进行扩展,从系统层面解决项目中的一些设计或技术问题。
什么是RunTime
Objective-C 扩展了 C 语言,并加入了面向对象特性和 Smalltalk 式的消息传递机制。而这个扩展的核心是一个用 C 和 汇编 写的 Runtime 库。它是 Objective-C 面向对象和动态机制的基石。苹果和GNU各自维护一个开源的 runtime 版本,这两个版本之间都在努力的保持一致。
Objective-C 是一个动态语言,这意味着它不仅需要一个编译器,也需要一个运行时系统(runtime)来动态的 创建类和对象、进行消息传递和转发。
高级语言想要成为可执行文件需要先编译为汇编语言再汇编为机器语言,机器语言也是计算机能够识别的唯一语言。
OC –> runtime –> C –> 编译汇编 –> 机器语言
从OC到C语言的过渡就是由runtime来实现的。然而我们使用OC进行面向对象开发,而C语言更多的是面向过程开发,这就需要将面向对象的类转变为面向过程的结构体。
消息传递——RunTime的核心
在Objective-C中,使用[receiver message]
语法并不会马上执行receiver
对象的message
方法的代码,而是向receiver
发送一条message
消息,这条消息可能由receiver
来处理,也可能由转发给其他对象来处理,也有可能假装没有接收到这条消息而没有处理。
如下:
一个对象的方法像这样[obj foo]
,编译器转成消息发送objc_msgSend(obj, foo)
由此,其实[receiver message]
被编译器转化为:
1 | OBJC_EXPORT id objc_msgSend(id self, SEL op, ...) |
这就是一个标准C语言函数的形式,其中两个数据类型:id 和 SEL 是实现的关键。
id
接下来看objc_msgSend
第一个参数的数据类型id,id是通用类型指针,能够表示任何对象.
查看到id数据结构如下1
2
3
4
5
6
7
8
9
10
11
12
/// An opaque type that represents an Objective-C class.--> 类
typedef struct objc_class *Class;
/// Represents an instance of a class. --> 类的实例(对象)
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class. --> id
typedef struct objc_object *id;
id
其实就是一个指向objc_object
结构体指针,它包含一个Class isa
成员,根据isa
指针就可以顺藤摸瓜找到对象所属的类。
接下来就可以详细分析类和对象的结构了
类和对象的结构
我们看看对象(object),类(class)这两个结构体:
对象 objc_object
对象的构成比较简单:
1 | /// Represents an instance of a class. --> 类的实例(对象) |
其中的Class,即objc_class结构体,可以在runtime.h文件中找到类结构体的定义。
类 objc_class
查看到objc_class结构体定义如下:
1 | struct objc_class { |
分析一些重要的成员变量表示什么意思和对应使用哪些数据结构。
isa
isa
表示一个Class对象的Class,也就是Meta Class
。在面向对象设计中,一切都是对象,Class在设计中本身也是一个对象。我们会在objc-runtime-new.h
文件找到证据,发现objc_class
有以下定义:
1 | struct objc_class : objc_object { |
由此可见,结构体objc_class也是继承objc_object,说明Class在设计中本身也是一个对象。
其实Meta Class
也是一个Class
,那么它也跟其他Class
一样有自己的isa
和super_class
指针,使用它们可以确定一个类或者对象所属的类型:

上图实线是super_class
指针,虚线是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)。
super_class
super_class
表示实例对象对应的父类,与前面讨论的meta class
中相同
name
name
字符串表示类名
ivars
ivars
表示多个成员变量(属性),它指向objc_ivar_list
结构体。在runtime.h
可以看到它的定义:
1 | struct objc_ivar_list { |
objc_ivar_list
其实就是一个链表,存储多个objc_ivar
,而objc_ivar
结构体存储 类的单个成员变量 信息。
methodLists
methodLists
表示方法列表,它指向objc_method_list
结构体的二级指针.
可以动态修改 *methodLists 的值来添加成员方法,也是Category实现原理,同样也解释Category不能直接添加实例变量的原因。
在runtime.h可以看到它的定义:
1 | struct objc_method_list { |
同理,objc_method_list
也是一个链表,存储多个objc_method
,而objc_method
结构体存储类的某个方法的信息:
1 | /// An opaque type that represents a method in a class definition. |
其实Method
就是一个指向objc_method
结构体指针,它存储了
- 方法名(
method_name
) SEL类型 - 方法类型(
method_types
) 字符串 - 方法实现(
method_imp
) IMP类型
而method_imp的数据类型是IMP,它是一个函数指针,后面会重点提及。
cache
cache
用来缓存经常访问的方法,它指向objc_cache
结构体
protocols
protocols
表示类遵循哪些协议
以上通过类的结构分析,我们搞明白了对象和类的构成,以及id、类、对象之间的关系。
回到消息传递那里,另一个参数是 SEL
,表示方法。在Method
中,也看到了这个关键字。SEL
和IMP
其实都是Method
的属性。
方法
方法 Method
即 objc_method
SEL(objc_selector)
objc_msgSend
函数第二个参数类型为SEL
,它是selector
在OC中的表示类型(Swift中是Selector
类)。selector
是方法选择器,可以理解为区分方法的 ID,而这个 ID 的数据结构是SEL
。
先看下定义
1 | Objc.h |
其实selector就是个映射到方法的C字符串,你可以用 Objective-C 编译器命令@selector()或者 Runtime 系统的sel_registerName函数来获得一个 SEL 类型的方法选择器。
IMP
在上面讲Method
时就说过,IMP
本质上就是一个函数指针,指向方法的实现,在objc.h
找到它的定义:
1 | /// A pointer to the function of a method implementation. |
当你向某个对象发送一条信息,可以由IMP这个函数指针来指定方法的实现,它最终就会执行那段代码,这样可以绕开消息传递阶段而去执行另一个方法实现。
在iOS的runtime中,Method通过selector和IMP两个属性,实现了快速查询方法及实现,相对提高了性能,又保持了灵活性。
Cache(objc_cache) 缓存
顾名思义,Cache主要用来缓存,那它缓存什么呢?我们先在runtime.h文件看看它的定义:
1 | typedef struct objc_cache *Cache OBJC2_UNAVAILABLE; |
Cache其实就是一个存储Method的链表,主要是为了优化方法调用的性能。当对象receiver调用方法message时,首先根据对象receiver的isa指针查找到它对应的类,然后在类的methodLists中搜索方法,如果没有找到,就使用super_class指针到父类中的methodLists查找,一旦找到就调用方法。如果没有找到,有可能消息转发,也可能忽略它。但这样查找方式效率太低,因为往往一个类大概只有20%的方法经常被调用,占总调用次数的80%。所以使用Cache来缓存经常调用的方法,当调用方法时,优先在Cache查找,如果没有找到,再到methodLists查找。