简介
Objective-C 是一个动态语言,这意味着它不仅需要一个编译器,也需要一个运行时系统来动态得创建类和对象、进行消息传递和转发。理解 Objective-C 的 Runtime 机制可以帮我们更好的了解这个语言,适当的时候还能对语言进行扩展,从系统层面解决项目中的一些设计或技术问题。了解 Runtime ,要先了解它的OC中类和对象的结构 然后了解消息传递 (Messaging)。
OC中类和对象的结构
Objective-C类是由Class类型来表示的,它实际上是一个指
向objc_class结构体的指针。1
typedef struct object_class *Class
查看objc/runtime.h中objc_class结构体的定义如下:
1 | struct object_class{ |
objc_object
objc_object是表示一个类的实例的结构体
它的定义如下(objc/objc.h):
1 | struct objc_object{ |
这个结构体只有一个指针,即指向其类的isa指针。这
样,当我们向一个Objective-C对象发送消息时,运行时库会根据
实例对象的isa指针找到这个实例对象所属的类。Runtime库会在类的方法列表及父类的方法列表中去寻找与消息对应的selector指向的方法,找到后即运行这个方法。
元类(Meta Class)
objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类。
方法与消息
SEL
SEL又叫选择器,是表示一个方法的selector的指针,其定义如下:
1 | typedef struct objc_selector *SEL; |
方法的selector用于表示运行时方法的名字。Objective-C在编译时,会依据每一个方法的名字、参数序列,生成一个唯一的整型标识(Int类型的地址),这个标识就是SEL。
两个类之间,只要方法名相同,那么方法的SEL就是一样的,每一个方法都对应着一个SEL。所以在Objective-C同一个类(及类的继承体系)中,不能存在2个同名的方法,即使参数类型不同也不行
当然,不同的类可以拥有相同的selector,这个没有问题。不同类的实例对象执行相同的selector时,会在各自的方法列表中去根据selector去寻找自己对应的IMP。
工程中的所有的SEL组成一个Set集合,如果我们想到这个方法集合中查找某个方法时,只需要去找到这个方法对应的SEL就行了,SEL实际上就是根据方法名hash化了的一个字符串,而对于字符串的比较仅仅需要比较他们的地址就可以了,可以说速度上无语伦比!
本质上,SEL只是一个指向方法的指针(准确的说,只是一个根据方法名hash化了的KEY值,能唯一代表一个方法),它的存在只是为了加快方法的查询速度。
通过下面三种方法可以获取SEL:
- sel_registerName函数
- Objective-C编译器提供的@selector()
- NSSelectorFromString()方法
IMP
IMP实际上是一个函数指针,指向方法实现的地址。
其定义如下:
1 | id (*IMP)(id, SEL,...) |
第一个参数:是指向self的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针)
第二个参数:是方法选择器(selector)
接下来的参数:方法的参数列表。
前面介绍过的SEL就是为了查找方法的最终实现IMP的。由于每个方法对应唯一的SEL,因此我们可以通过SEL方便快速准确地获得它所对应的IMP。取得IMP后,我们就获得了执行这个方法代码的入口点,此时,我们就可以像调用普通的C语言函数一样来使用这个函数指针了。
Method
Method用于表示类定义中的方法,则定义如下:
1 | typedef struct objc_method *Method |
我们可以看到该结构体中包含一个SEL和IMP,实际上相当于在SEL和IMP之间作了一个映射。有了SEL,我们便可以找到对应的IMP,从而调用方法的实现代码。
方法调用流程
在Objective-C中,消息直到运行时才绑定到方法实现上。编译器会将消息表达式[receiver message]转化为一个消息函数的调用,即objc_msgSend。这个函数将消息接收者和方法名作为其基础参数,如以下所示
1 | objc_msgSend(receiver, selector) |
如果消息中还有其它参数,则该方法的形式如下所示:
1 | objc_msgSend(receiver, selector, arg1, arg2,...) |
这个函数完成了动态绑定的所有事情.
基本流程就是:
objc_msgSend通过对象的isa指针获取到类的结构体,然后在方法的缓存列表里面查找方法的selector。如果缓存里没有再去方法列表里去找。
如果没有找到selector,objc_msgSend结构体中的指向父类的指针找到其父类,并在父类的缓存里面查找方法的selector。
依此,会一直沿着类的继承体系到达NSObject类。一旦定位到selector,函数会就获取到了实现的入口点,并传入相应的参数来执行方法的具体实现,并将该方法添加进入缓存中如果最后没有定位到selector,则会走消息转发流程。
消息转发
当一个对象能接收一个消息时,就会走正常的方法调用流程。但如果一个对象无法接收指定消息时,也就是没有找到方法的实现时,通常情况下,程序会在运行时挂掉并抛出 unrecognized selector sent to … 的异常。但在异常抛出前,Objective-C 的运行时会给你三次拯救程序的机会:
- Method resolution
- Fast forwarding
- Normal forwarding
以objc_msgSend(obj, foo)为例来分析下三种方式
Method resolution
首先,Objective-C 运行时会调用 +resolveInstanceMethod: 或者 +resolveClassMethod:,让你有机会提供一个函数实现。如果你添加了函数并返回 YES, 那运行时系统就会重新启动一次消息发送的过程。你可以这么实现:
1 | void fooMethod(id obj, SEL _cmd) |
如果 resolve 方法返回 NO ,运行时就会移到下一步:消息转发(Message Forwarding)。
Fast forwarding
如果目标对象实现了 -forwardingTargetForSelector: ,Runtime 这时就会调用这个方法,给你把这个消息转发给其他对象的机会。
1 | - (id)forwardingTargetForSelector:(SEL)aSelector |
只要这个方法返回的不是 nil 和 self,整个消息发送的过程就会被重启,当然发送的对象会变成你返回的那个对象。否则,就会继续 Normal Fowarding 。
这里叫 Fast ,只是为了区别下一步的转发机制。因为这一步不会创建任何新的对象,但下一步转发会创建一个 NSInvocation 对象,所以相对更快点。
Normal forwarding
这一步是 Runtime 最后一次给你挽救的机会。首先它会发送 -methodSignatureForSelector: 消息获得函数的参数和返回值类型。如果 -methodSignatureForSelector: 返回 nil ,Runtime 则会发出 -doesNotRecognizeSelector: 消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime 就会创建一个 NSInvocation 对象并发送 -forwardInvocation: 消息给目标对象。
NSInvocation 实际上就是对一个消息的描述,包括selector 以及参数等信息。所以你可以在 -forwardInvocation: 里修改传进来的 NSInvocation 对象,然后发送 -invokeWithTarget: 消息给它,传进去一个新的目标:
1 | - (void)forwardInvocation:(NSInvocation *)invocation |