Objective-C Runtime 消息传递与转发

概述

Objective-C 本质上是一种基于 C 语言的领域特定语言。Objective-C 通过一个用 C 语言和汇编实现的 runtime,在 C 语言的基础上实现了面向对象的功能。在 runtime 中,对象用结构体表示,方法用函数表示。

C 语言是一门静态语言,其在编译时决定调用哪个函数。而 Objective-C 则是一门动态语言,其在编译时不能决定最终执行时调用哪个函数(Objective-C 中函数调用称为消息传递)。Objective-C 的这种动态绑定机制正是通过 runtime 这样一个中间层实现的。

为了分析 runtime 是如何进行动态绑定,我们首先需要了解一下 Objective-C 中类与对象等基本结构在 C 语言层面是如何实现的。

数据结构

Objective-C 类

Objective-C 类是由 Class 类型表示的,它本质上是一个指向 objc_class 结构体的指针。如下所示为 objc/runtime.h 中关于类的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct object_class *Class

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;
/* Use `Class` instead of `struct objc_class *` */

Objective-C 对象

Objective-C 对象是由 id 类型表示的,它本质上是一个指向 objc_object 结构体的指针。如下所示为 objc/objc.h 中关于对象的定义:

1
2
3
4
5
typedef struct objc_object *id;

struct objc_object{
Class isa OBJC_ISA_AVAILABILITY;
};

objc_object 结构体中只有一个成员,即指向其类的 isa 指针。当向一个 Objective-C 对象发送消息时,runtime 会根据实例对象的 isa 指针找到其所属的类。Runtime 会在类的方法列表以及父类的方法列表中去寻找与消息对应的 selector 指向的方法,找到后即运行该方法。

Objective-C 元类(meta class)

meta-class 是一个类对象的类。在 Objective-C 中,所有的类本身也是一个对象。事实上,在很多原型编程语言也采用这种“万物皆对象”的设计思想,如:Io。

通过向该对象发送消息,即可实现对类方法的调用。前提是类的 isa 指针必须指向一个包含这些类方法的 objc_class 结构体。meta-class 中存储着一个类的所有类方法。所以,类对象的 isa 指针指向的就是 meta-class

  • 当向一个对象发送消息时,runtime 会在这个对象所属的类的方法列表中查找方法。
  • 当向一个类发送消息时,runtime 会在这个类的 meta-class 的方法列表中查找。

思考一下,meta-class 也是一个类,也可以向它发送一个消息,那么它的 isa 又是指向什么呢?为了不让这种结构无限延伸下去,Objective-C 的设计者让所有的 meta-classisa 指向基类的 meta-class,以此作为它们的所属类。

下图所示,为 Objective-C 对象在内存中的引用关系图。

Objective-C 方法

方法实际上是一个指向 objc_method 结构体的指针。如下所示为 objc/runtime.h 中关于方法的定义:

1
2
3
4
5
6
7
typedef struct objc_method *Method

struct objc_method{
SEL method_name OBJC2_UNAVAILABLE; // 方法名
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE; // 方法实现
}
结构体中包含成员 SELIMP,两者将方法的名字与实现进行了绑定。通过 SEL,可以找到对应的 IMP,从而调用方法的具体实现。

SEL

SEL 又称选择器,是一个指向 objc_selector 结构体的指针。

1
typedef struct objc_selector *SEL;

方法的 selector 用于表示运行时方法的名字。Objective-C 在编译时,会根据方法的名字(不包括参数)生成一个唯一的整型标识( Int 类型的地址),即 SEL

由于一个类的方法列表中不能存在两个相同的 SEL,所以 Objective-C 不支持重载。但是不同类之间可以存在相同的 SEL,因为不同类的实例对象执行相同的 selector 时,会在各自的方法列表中去根据 selector 去寻找自己对应的 IMP

通过下面三种方法可以获取 SEL

  • sel_registerName 函数
  • Objective-C 编译器提供的 @selector() 方法
  • NSSeletorFromString() 方法

IMP

IMP 本质上就是一个函数指针,指向方法实现的地址。

1
typedef id (*IMP)(id, SEL,...);

参数说明

  • id:指向 self 的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针)
  • SEL:方法选择器
  • ...:方法的参数列表

SELIMP 的关系类似于哈希表中 keyvalue 的关系。采用这种哈希映射的方式可以加快方法的查找速度。

消息传递(方法调用)

在 Objective-C 中,消息直到运行时才绑定到方法实现上。编译器会将消息表达式 [receiver message] 转化为一个消息函数的调用,即 objc_msgSend。这个函数将消息接收者和方法名作为主要参数,如下所示:

1
2
objc_msgSend(receiver, selector)                    // 不带参数
objc_msgSend(receiver, selector, arg1, arg2,...) // 带参数

objc_msgSend 通过以下几个步骤实现了动态绑定机制。

  • 首先,获取 selector 指向的方法实现。由于相同的方法可能在不同的类中有着不同的实现,因此根据 receiver 所属的类进行判断。
  • 其次,传递 receiver 对象、方法指定的参数来调用方法实现。
  • 最后,返回方法实现的返回值。

消息传递的关键在于前文讨论过的 objc_class 结构体,其有两个关键的字段:

  • isa:指向父类的指针
  • methodLists: 类的方法分发表(dispatch table

当创建一个新对象时,先为其分配内存,并初始化其成员变量。其中 isa 指针也会被初始化,让对象可以访问类及类的继承链。

下图所示为消息传递过程的示意图。

  • 当消息传递给一个对象时,首先从运行时系统缓存 objc_cache 中进行查找。如果找到,则执行。否则,继续执行下面步骤。
  • objc_msgSend 通过对象的 isa 指针获取到类的结构体,然后在方法分发表 methodLists 中查找方法的 selector。如果未找到,将沿着类的 isa 找到其父类,并在父类的分发表 methodLists 中继续查找。
  • 以此类推,一直沿着类的继承链追溯至 NSObject 类。一旦找到 selector,传入相应的参数来执行方法的具体实现,并将该方法加入缓存 objc_cache 。如果最后仍然没有找到 selector,则会进入消息转发流程(下文将进行介绍)。

消息转发

当一个对象能接收一个消息时,会走正常的消息传递流程。当一个对象无法接收某一消息时,会发生什么呢?默认情况下,如果以 [object message] 的形式调用方法,如果 object 无法响应 message 消息时,编译器会报错。如果是以 performSeletor: 的形式调用方法,则需要等到运行时才能确定 object 是否能接收 message 消息。如果不能,则程序崩溃。

对于后者,当不确定一个对象是否能接收某个消息时,可以调用 respondsToSelector: 来进行判断。

1
2
3
if ([self respondsToSelector:@selector(method)]) {
[self performSelector:@selector(method)];
}

事实上,当一个对象无法接收某一消息时,就会启动所谓“消息转发(message forwarding)”机制。通过消息转发机制,我们可以告诉对象如何处理未知的消息。

消息转发机制大致可分为三个步骤:

  • 动态方法解析(Dynamic Method Resolution)
  • 备用接收者
  • 完整消息转发

下图所示为消息转发过程的示意图。

动态消息解析

对象在接收到未知的消息时,首先会调用所属类的类方法 +resolveClassMethod: 或实例方法 +resolveInstanceMethod:

在这两个方法中,我们可以为未知消息新增一个“处理方法”,通过运行时 class_addMethod 函数动态添加到类中。比如:

1
2
3
4
5
6
7
8
9
10
11
void dynamicMethodIMP(id self, SEL _cmd) {
// 方法实现
}

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

备用接收者

如果在上一步无法处理消息,则 runtime 会继续调用 - (id)forwardingTargetForSelector:(SEL)aSelector 方法。

如果一个对象实现了这个方法,并返回一个非 nil(也不能是 self) 的对象,则这个对象会称为消息的新接收者,消息会被分发到这个对象。比如:

1
2
3
4
5
6
7
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSString * selString = NSStringFromSelector(aSelector);
if ([selString isEqualToString:@"walk"]) {
return self.otherObject;
}
return [super forwardingTargetForSelector:aSelector];
}

这一步合适于我们只想将消息转发到另一个能处理该消息的对象上。但这一步无法对消息进行处理,如操作消息的参数和返回值。

完整消息转发

如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。 这一步调用 methodSignatureForSelector 进行方法签名,这可以将函数的参数类型和返回值封装。如果返回 nil,则说明消息无法处理并报错 unrecognized selector sent to instance

1
2
3
4
5
6
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if ([NSStringFromSelector(aSelector) isEqualToString:@"testInstanceMethod"]){
return [NSMethodSignature signatureWithObjcTypes:"v@:"];
}
return [super methodSignatureForSelector: aSelector];
}

如果返回 methodSignature,则进入 forwardInvocation。对象会创建一个表示消息的 NSInvocation 对象,把与尚未处理的消息有关的全部细节都封装在 anInvocation 中,包括 selectortarget,参数。在这个方法中可以修改实现方法,修改响应对象等,如果方法调用成功,则结束。如果依然不能正确响应消息,则报错 unrecognized selector sent to instance

1
2
3
4
5
- (void)forwardInvovation:(NSInvocation)anInvocation {
[anInvocation invokeWithTarget:_helper];
[anInvocation setSelector:@selector(run)];
[anInvocation invokeWithTarget:self];
}

可以利用备用接受者和完整消息转发实现对接受消息对象的转移,可以实现“多重继承”的效果。

参考

  1. Objective-C Runtime Programming Guide
  2. Objective-C Runtime · 笔试面试知识整理
  3. iOS运行时(Runtime)详解+Demo
  4. iOS内功篇:runtime