由上面一章中,我们了解了什么是RunTime,RunTime
用来做什么,下面了解一下Runtime
数据结构。
我们知道在Objective-C
中,使用[object doSomething]
语法并不会马上执行object
接受者对象的doSomething
方法的代码,而是向object
接受者对发送一条doSomething
消息,这条消息可能由object
接受者对来处理,也可能由转发给其他对象来处理,也有可能假装没有接收到这条消息而没有处理。
其实[object doSomething]
被编译器转化为:
id objc_msgSend ( id self, SEL op, ... );
下面从两个数据结构id
和SEL
来逐步分析和理解Runtime
有哪些重要的数据结构。
id
objc_msgSend
方法里面的第一个参数的数据类型id
,通用类型指针,能够表示任何对象。
查看源文件,可以看出id
其实就是一个指向objc_object
结构体指针,它包含一个Class isa
成员,根据Class isa
指针就可以找到对象所属的类。
Class
从源文件看出,Objective-C
的对象就是一个包含isa
指针的数据结构,而isa
指针的数据类型是Class
,Class
表示对象所属的类。
从源文件看出,Class
其实就是一个objc_class
结构体指针。objc_class
结构体定义如下:
isa:在Objective-C中,所有的类自身也是一个对象,即类对象。在这个类对象里面也有一个isa指针,它指向metaClass(元类)。
super_class:指向该类的父类,如果该类已经是最顶层的根类(如NSObject),则super_class为NULL。
name:这个类的类名。
version:提供类的版本信息,这对于对象的序列化非常有用,它可是让我们识别出不同类定义版本中实例变量布局的改变。
info:类信息,供运行期使用的一些位标识。
instance_size:该类的实例变量大小。
ivars:该类的成员变量链表。
methodLists:方法定义的链表。
protocols:协议链表。
cache:一个接收者对象接收到一个消息时,它会根据isa指针去查找能够响应这个消息的对象。在实际使用中,这个对象只有一部分方法是常用的,很多方法其实很少用或者根本用不上。这种情况下,如果每次消息来时,我们都是methodLists中遍历一遍,性能势必很差。这时,cache就派上用场了。在我们每次调用过一个方法后,这个方法就会被缓存到cache列表中,下次调用的时候runtime就会优先去cache中查找,如果cache没有,才去methodLists中查找方法。这样,对于那些经常用到的方法的调用,但提高了调用的效率。
注意:
- 在面向对象设计中,一切都是对象,
Class
在设计中本身也是一个对象。 - 由此可见,结构体
objc_class
也是继承objc_object
,说明Class
在设计中本身也是一个对象。
元类(Meta Class)
在Objective-C
中,所有的类自身也是一个对象,这个对象里面也有一个isa
指针,它指向metaClass
(元类),向这个对象发送消息(即调用类方法)。
从图中看出:
- 当发送一个实例方法的消息时,
isa
指针会在这个类的实例方法列表中查找; - 当发送一个类方法的消息时,
isa
指针会在这个类的meta-class
的方法列表中查找。meta-class
之所以重要,是因为它存储着一个类的所有类方法。 - 每个类都会有一个单独的
meta-class
,因为每个类的类方法基本不可能完全相同。
Objective-C的设计者让所有的meta-class的isa指向基类的meta-class,以此作为它们的所属类。即,任何NSObject继承体系下的meta-class都使用NSObject的meta-class作为自己的所属类,而基类的meta-class的isa指针是指向它自己,这样就形成了一个完美的闭环。
SEL
objc_msgSend
函数第二个参数类型为SEL
,它是selector
在Objc
中的表示类型(Swift
中是Selector
类)。selector
是方法选择器,可以理解为区分方法的id
,而这个id
的数据结构是SEL
,即表示一个方法的selector
的指针。
- 方法的
selector
用于表示运行时方法的名字,Objective-C
在编译时,会依据每一个方法的名字、参数序列,生成一个唯一的整型标识(int
类型的地址),这个标识就是SEL
。 - 在
Objective-C
中,只要方法名相同,那么方法的SEL
就是一样的,每一个方法都对应着一个SEL
,所以在Objective-C
中,同一个类中或者这个类的继承体系中,不能存在2个同名的方法,不同的类可以拥有相同的selector
,不同的类的实例对象执行相同的selector
,会在各自的方法列表中根据selector
去寻找对应的IMP
。 - 在本质上,
SEL
只是一个指向方法的指针(被hash
化得KEY
值),能提高方法的查询速度。
IMP
IMP实际上是一个函数指针,指向方法实现的首地址。其定义如下:
id (*IMP)(id, SEL, ...)
第一个参数是指向 self
的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针),第二个参数是方法选择器( selector
),接下来是方法的实际参数列表。
SEL
就是为了查找方法的最终实现IMP
的,由于每个方法对应唯一的SEL
,因此我们可以通过SEL
方便快速准确地获得它所对应的IMP
。
Method
Method
是一种代表类中的某个方法的类型。
而objc_method
在上面的方法列表中提到过,它存储了方法名,方法类型和方法实现:
注意:
- 方法名类型为
SEL
,前面提到过相同名字的方法即使在不同类中定义,它们的方法选择器也相同。 - 方法类型
method_types
是个char
指针,其实存储着方法的参数类型和返回值类型。 -
method_imp
指向了方法的实现,本质上是一个函数指针。
Ivar
Ivar
是一种代表类中实例变量的类型。
Cache
Paste_Image.pngCache
其实就是一个存储Method
的链表,主要是为了优化方法调用的性能。
当对象receiver
调用方法message
时,首先根据对象receiver
的isa
指针查找到它对应的类,然后在类的methodLists
中搜索方法,如果没有找到,就使用super_class
指针到父类中的methodLists
查找,一旦找到就调用方法。如果没有找到,有可能消息转发,也可能忽略它。但这样查找方式效率太低,因为往往一个类大概只有20%的方法经常被调用,占总调用次数的80%。所以使用Cache
来缓存经常调用的方法,当调用方法时,优先在Cache
查找,如果没有找到,再到methodLists
查找。
网友评论