iOS runtime详解

简介

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
2
3
4
5
6
7
8
9
10
11
12
13
14
struct object_class{
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE; // 父类
const char *name OBJC2_UNAVAILABLE; // 类名
long version OBJC2_UNAVAILABLE; // 类的版本信息,默认为0
long info OBJC2_UNAVAILABLE; // 类信息,供运行期使用的一些位标识
long instance_size OBJC2_UNAVAILABLE; // 该类的实例变量大小
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; // 该类的成员变量链表
struct objc_method_list *methodLists OBJC2_UNAVAILABLE; // 方法定义的链表
struct objc_cache *cache OBJC2_UNAVAILABLE; // 方法缓存
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 协议链表
#endif
}OBJC2_UNAVAILABLE;

objc_object

objc_object是表示一个类的实例的结构体
它的定义如下(objc/objc.h):

1
2
3
4
struct objc_object{
Class isa OBJC_ISA_AVAILABILITY;
};
typedef struct objc_object *id;

这个结构体只有一个指针,即指向其类的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
2
3
4
5
6
typedef struct objc_method *Method
struct objc_method{
SEL method_name OBJC2_UNAVAILABLE; // 方法名
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE; // 方法实现
}

我们可以看到该结构体中包含一个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
2
3
4
5
6
7
8
9
10
11
12
13
void fooMethod(id obj, SEL _cmd)
{
NSLog(@"Doing foo");
}

+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
if(aSEL == @selector(foo:)){
class_addMethod([self class], aSEL, (IMP)fooMethod, "v@:");
return YES;
}
return [super resolveInstanceMethod];
}

如果 resolve 方法返回 NO ,运行时就会移到下一步:消息转发(Message Forwarding)。

Fast forwarding

如果目标对象实现了 -forwardingTargetForSelector: ,Runtime 这时就会调用这个方法,给你把这个消息转发给其他对象的机会。

1
2
3
4
5
6
7
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if(aSelector == @selector(foo:)){
return alternateObject;
}
return [super forwardingTargetForSelector:aSelector];
}

只要这个方法返回的不是 nil 和 self,整个消息发送的过程就会被重启,当然发送的对象会变成你返回的那个对象。否则,就会继续 Normal Fowarding 。

这里叫 Fast ,只是为了区别下一步的转发机制。因为这一步不会创建任何新的对象,但下一步转发会创建一个 NSInvocation 对象,所以相对更快点。

Normal forwarding

这一步是 Runtime 最后一次给你挽救的机会。首先它会发送 -methodSignatureForSelector: 消息获得函数的参数和返回值类型。如果 -methodSignatureForSelector: 返回 nil ,Runtime 则会发出 -doesNotRecognizeSelector: 消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime 就会创建一个 NSInvocation 对象并发送 -forwardInvocation: 消息给目标对象。

NSInvocation 实际上就是对一个消息的描述,包括selector 以及参数等信息。所以你可以在 -forwardInvocation: 里修改传进来的 NSInvocation 对象,然后发送 -invokeWithTarget: 消息给它,传进去一个新的目标:

1
2
3
4
5
6
7
8
9
10
11
- (void)forwardInvocation:(NSInvocation *)invocation
{
SEL sel = invocation.selector;

if([alternateObject respondsToSelector:sel]) {
[invocation invokeWithTarget:alternateObject];
}
else {
[self doesNotRecognizeSelector:sel];
}
}

参考

iOS运行时(Runtime)详解+Demo