基本简介
1、根据官方文档,OC有一个特性:它会尽可能把一些决定从编译时和链接时推迟到运行时才处理,所以这门语言需要的就不只是一个编译器,它还需要一个runtime系统来处理那些已经被编译过的代码。
2、runtime有两种:legacy runtime和modern runtime,区别在于:
(1)、在legacy runtime中,如果你修改了一个类的实例变量之后,你需要重新编译一下;
(2)、在modern runtime中,在这种情况下你无需再重新编译一次。
3、OC的程序和runtime系统有三个层次的交互方式:
(1)、大部分时候,当你直接使用OC代码的时候,runtime系统便在运作了。这其中一个最主要的功能便是消息发送(Messaging);
(2)、使用一些NSObject类的方法,比如description或isKindOfClass: 、isMemberOfClass:等方法(其实大部分的类都是继承自NSObject,都可以使用这些方法,也有一些继承自其它类,比如继承自NSProxy类);
(3)、直接调用runtime提供的方法。
消息发送(Messaging)的概念
4、在讨论消息发送之前,我们先要明白一个概念:方法的selector是什么?
方法的selector是根据这个方法的方法名计算得出的一个key,通过这个key可以得到反推出这个方法的方法名。由于selector是根据方法名得出的,所以同名的方法的selector都是相同的。
而在一个类中,为了让一个selector可以唯一标识出一个方法,于是在同一个类里就不允许有两个同名的方法,即使参数不同也不行。(所以OC没有方法重载)
方法除了selector之外,另外还有一个组成部分是方法实现(procedure ,也即是method implementation),指的是方法的实现代码。
所以可以这么理解:方法就是它的selector和实现的统称。
同时需要注意的是,一个方法的selector和实现之间并不是固定对应的,消息发送的过程其实就是通过方法的selector找到它的实现的过程。
5、消息发送的基本流程:
在OC里面,当我们“调用”方法的时候,使用的是:
[receiver message]
这样的语法,这句代码其实并不会立刻就去执行message方法的代码,在这句代码和message方法的代码之间,有一个消息发送(Messaging)的过程。
编译器把这一句代码编译成了:
objc_msgSend(receiver, selector)
如果方法有多个参数的话,会连同参数被编译成:
objc_msgSend(receiver, selector, arg1, arg2, ...)
然后这个objc_msgSend()函数执行的过程便是消息发送的过程了。
消息其实就是objc_msgSend()函数的所有参数的并称,可以理解为:消息(Message)就是方法调用者、方法的selector和方法的参数等细节的集合。
消息发送的过程其实就是objc_msgSend()函数会根据receiver和selector参数找到对应的方法实现代码(procedure ,也即是method implementation)并执行它的过程。
在这个过程中,objc_msgSend()函数是如何找到对应的类,找到对应的方法实现的呢?并且如果在查找的过程中发生了意外的话,它又会怎么处理呢?
在研究这些问题之前,我们先要了解一下runtime相关的一些数据结构。
runtime相关的数据结构
6、我们从objc_msgSend()这个方法的定义入手,来研究一下这些数据结构,objc_msgSend()的定义如下:
id objc_msgSend(id self, SEL op, ...)
它有两个主要的参数,一个是id类型的,一个SEL类型的。
(1)、首先来看SEL这个类型,我们可以找到它的定义如下:
没有找到struct objc_selector进一步的定义,但是我们通过字面可以知道这个结构体便是方法,或者是能够唯一标识出这个方法的一些信息,而SEL作为一个指向selector的指针,便可理解为指向某个方法的指针了;
(2)、objc_msgSend()的另一个主要参数是id类型的,我们可以找到id类型的定义如下:
所以id其实是一个指向struct objc_object结构体的指针,而struct objc_object这个结构体包含了一个成员isa,这个成员的数据类型是Class,那么接下来就来看看Class是什么;
(3)、Class的定义如下:
我们可以知道,Class是一个指向struct objc_class结构体的指针,再进一步找到struct objc_class结构体的定义,如下:
可以看到struct objc_class结构体包含了很多成员,我们一个一个来查看这些成员。
7、struct objc_class:
(1)、首先我们可以神奇地发现,struct objc_class结构体的第一个成员的数据类型竟然也是Class,由上面我们已经知道Class是一个指向struct objc_class结构体的指针,所以struct objc_class结构体的第一个成员也是一个同样叫做isa 的struct objc_class结构体。
这说明了,类本身也是某种类的实例。这个先按下不表,在后文(9)中再做讨论;
(2)、struct objc_class结构体(以下直接称为“类”)的第二个成员仍然是一个Class类型的数据super_class,表示这个结构体对象对应的父类;
(3)、version和info都是存储着类相关的信息,当某个类的实例变量发生变化的时候,它的version的内容会跟着发生变化;
(4)、instance_size是这个类生成的实例对象的大小;
(5)、成员ivars指针指向了struct objc_ivar_list结构体类型的数据,我们可以找到struct objc_ivar_list结构体这种数据的定义:
可以看到其中最主要的成员是一个由struct objc_ivar结构体组成的数组ivar_list[],它便是这个类所包含的所有实例变量组成数组。
我们还可以进一步查看到struct objc_ivar结构体的定义:
所以其实一个struct objc_ivar结构体便是一个成员变量;
(6)、成员methodLists和ivars类似,我们先看一下struct objc_method_list这种数据类型的定义:
结构和struct objc_ivar_list基本相似,主要内容也是一个数组,由struct objc_method类型的数据组成,我们再看一下struct objc_method的定义:
可以发现struct objc_method类型的数据便是其实便是方法了。这个结构体最主要的成员是一个IMP类型的数据method_imp,它其实是一个指向方法实现代码的指针。
同时,可以注意到成员methodLists是一个二级指针,这决定了它可以更改指向的*methodLists,即是可以修改方法列表,这也是Category可以添加方法的原因;
(7)、成员cache用来存储经常调用的method,当对某个对象调用方法时,runtime会根据对象的isa指针找到对象对应的类,然后首先在类的cache中查到要调用的方法,当cache中找不到之后,才会去methodLists中查找;
(8)、成员protocols标识这个类遵循的协议;
(9)、前面说到,类本身也是某种类的实例,我们从Class第一个成员isa也是一个Class类型的的数据的出这个结论的。
类所属的类,称为元类(Meta Class),元类存储着一个类的所有类方法,类是这个元类的实例。当我们调用类方法的时候,其实把这个类当做一个实例向它发送消息:当我们向一个对象发送消息时,runtime会在这个对象所属的这个类的方法列表中查找方法;而向一个类发送消息时,会在这个类的元类的方法列表中查找。
再继续深究下去,发现元类也是一个类,那么它的isa指针又该指向哪里呢?这样一直延伸下去会无穷无尽,所以所有元类的isa指针会被指向同一个类——基类(Root Class)的元类,基类的元类的isa指针又指向了它自己,形成了一个闭环。
需要清楚一个概念:元类(meta class)和父类(super class)是两个不同的概念,一般一个类都会有个一个元类和一个父类。
元类是这个类作为一个对象看待时,它所属的类;父类是这个类作为类看待时,它继承而来的类。
它们的关系如下图:
可以注意到,基类(Root Class)其实就是NSObject,它的superclass指向nil。NSObject的元类的父类指针指向NSObject,最终在顶部形成了一个回路。