Runtime学习:消息传递

Objective-C 扩展了 C 语言,并加入了面向对象特性和消息传递机制。而这个扩展的核心就是 Runtime 库。它是 Objective-C 面向对象和动态机制的基石。

基于我对 Runtime 的理解,我认为它的核心知识基本都围绕两个中心:

  • 消息传递
  • 类的动态配置

Runtime 的知识点比较多,计划用三篇文章来记录下自己的学习过程:

下面就根据这两个中心我们慢慢来学习 Runtime。首先我们需要对类的本质进行了解。

预备知识

类对象(objc_class)

Objective-C 类是由 Class 类型来表示的,它实际上是一个指向 objc_class 结构体的指针。

1
2
typedef struct objc_class *Class;

查看 objc/runtime.hobjc_class 结构体的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//runtime.h
struct objc_class {
// isa指针,指向元类(metaClass)
Class _Nonnull isa OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
// 父类
Class _Nullable super_class OBJC2_UNAVAILABLE;
// 类名
const char * _Nonnull name OBJC2_UNAVAILABLE;
// 类的版本信息,默认为0
long version OBJC2_UNAVAILABLE;
// 类信息
long info OBJC2_UNAVAILABLE;
// 该类的实例变量大小
long instance_size OBJC2_UNAVAILABLE;
// 该类的成员变量链表
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
// 该类的方法链表
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
// 方法缓存
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
// 协议链表
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

在 objc_class 的定义中,有几个我们比较感兴趣的对象:

isa 在 Objective-C 中,类自身也是一个对象,它的 isa 指针指向其 metaClass(元类)。

super_class 指向该类的父类,如果该类已经是最顶层的根类(如 NSObject),则super_class 为 NULL。

objc_method_list 该类中的所有实例方法链表。

objc_cache 实例调用过的方法缓存。

类缓存(objc_cache)

1
2
3
4
5
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method _Nullable buckets[1] OBJC2_UNAVAILABLE;
};

它包含了下面三个变量:

  • mask: 指定分配的缓存 bucket 的总数。,所以缓存的 size(total)是 mask+1。

  • occupied: 指定实际占用的缓存bucket的总数。

  • buckets: 指向 Method 数据结构指针的数组。

为了加速消息分发, 系统会对方法和对应的地址进行缓存,就放在 objc_cache 中,所以在实际运行中,大部分常用的方法都是会被缓存起来的。

Method(objc_method)

1
2
3
4
5
6
7
8
9
10
struct objc_method_list {
struct objc_method_list * _Nullable obsolete OBJC2_UNAVAILABLE;

int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
}

由结构定义可以看出 objc_method_list 是一个链表。

1
2
3
4
5
struct objc_method {
SEL _Nonnull method_name OBJC2_UNAVAILABLE;
char * _Nullable method_types OBJC2_UNAVAILABLE;
IMP _Nonnull method_imp OBJC2_UNAVAILABLE;
}
  • method_name 方法名
  • method_types 方法类型
  • method_imp 方法实现

关于方法类型,可以查看官方文档中的定义。

SEL(objc_selector)

方法选择器。是表示一个方法的 selector 的指针。

1
2
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

方法的 selector 用于表示运行时方法的名字。Objective-C 在编译时,会依据每一个方法的名字、参数序列,生成一个唯一的整型标识(Int类型的地址),这个标识就是SEL。

两个类之间,不管它们是父类与子类的关系,还是之间没有这种关系,只要方法名相同,那么方法的SEL就是一样的。每一个方法都对应着一个 SEL。所以在 Objective-C 同一个类(及类的继承体系)中,不能存在两个同名的方法,即使参数类型不同也不行。

举例:

1
2
- (void)addNum:(int)num;
- (void)addNum:(CGFloat)num;

这样的定义会导致编译错误,因为这样的 SEL 是相同的,并不能区分。需要改成下方的:

1
2
- (void)addIntNum:(int)num;
- (void)addCGFloadNum:(CGFloat)num;

当然,不同的类可以拥有相同的 selector,这个没有问题。不同类的实例对象执行相同的 selector 时,会在各自的方法列表中去根据 selector 去寻找自己对应的 IMP。

IMP

是一个函数指针,指向方法的实现。

1
2
3
4
5
6
/// A pointer to the function of a method implementation. 
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
#endif

第一个参数是指向 self 的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针),第二个参数是方法选择器(selector),接下来是方法的实际参数列表。

上面介绍的 SEL 就是为了查找方法的最终实现 IMP 的。由于每个方法对应唯一的 SEL,因此我们可以通过 SEL 方便快速准确地获得它所对应的 IMP。

实例(objc_object)

查看 objc/objc.hobjc_object 结构体的定义如下:

1
2
3
4
5
6
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;

可以看到,实例的定义中只有一个 isa 指针字段,它是指向本类的指针。根据 objc_class 定义可以得知关于这个对象的所有基本信息都存储在 objc_class 中。所以,objc_object 需要的就是一个指向其类对象的 isa 指针。这样当我们向一个 Objective-C 对象发送消息时,Runtime 会根据实例对象的 isa 指针找到这个实例对象所属的类。

元类(Meta Class)

当我们调用对象方法时(即给实例对象发送消息),是根据 isa 指针寻找到这个对象(objc_object)的类(objc_class),再寻找到对应的方法实现。对应的我们调用类方法时(即给类对象发送消息),也需要根据 isa 指针寻找到一个包含这些类方法的一个 objc_class 结构体。这就引出了 meta-class 的概念,元类中保存了创建类对象以及类方法所需的所有信息。

简单来说——元类是类对象的类。

元类,就像之前的类一样,它也是一个对象。你也可以调用它的方法。自然的,这就意味着他必须也有一个类。

任何 NSObject 继承体系下的 meta-class 都使用 NSObject 的 meta-class 作为自己的所属类,而基类的 meta-class 的 isa 指针是指向它自己。

方法传递

这里我们从实际的代码调用中来学习方法传递的全部过程。

简单的 Objective-C 代码调用:

1
[test testMethod];

利用 clang -rewrite-objc filename 代码转换为:

1
((void (*)(id, SEL))(void *)objc_msgSend)((id)test, sel_registerName("testMethod"));

可以看出

1
[test testMethod];

实质上就是

1
objc_msgSend((id)test, sel_registerName("testMethod"))

然后,我们可以在源码中查看 objc_msgSend 的执行步骤。由于源码是用汇编写的,这里就不贴出来了(主要是自己也看不懂汇编)。如果有兴趣的话,可以去下载官方源码在 objc_msg-xxx 文件中查看。

虽然,源码是用汇编写的,但是从注释中我们基本可以看出具体的执行步骤。

  1. 进入消息发送阶段,判断消息接受者是否为 nil。
  2. 利用 isa 指针找到自己的类对象。
  3. 在类对象的 objc_cache(方法缓存)中查找是否有方法,有则直接取出 Method(方法)中 IMP(实现)。无则继续。
  4. 在类对象的 objc_method_list 中查找方法。有则直接取出,无则继续。
  5. 找到类对象的 super_class ,继续在其父类中重复上面两个步骤进行查找。
  6. 若一直往上都没有找到方法的实现,那么消息发送阶段结束,接着会进入动态解析阶段,在这个阶段,若解析到方法,则结束。否则继续。
  7. 最后会进入消息转发阶段,在这里可以指定别的类为自己实现这个方法。
  8. 若上方步骤都没有找到方法的实现,则会报方法找不到的错误,无法识别消息,unrecognzied selector sent to instance。

上面8个步骤,可以看到消息传递的过程分为了以下三个阶段:

  • 消息发送阶段
  • 动态解析阶段
  • 消息转发阶段

消息发送阶段

从上方的分析可以得到: 方法查找的核心函数就是 _class_lookupMethodAndLoadCache3 函数,接下来重点分析 _class_lookupMethodAndLoadCache3 函数内的源码。

1
2
3
4
5
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

lookUpImpOrForward 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
bool initialize, bool cache, bool resolver)
{
// initialize = YES , cache = NO , resolver = YES
IMP imp = nil;
bool triedResolver = NO;

runtimeLock.assertUnlocked();

// 缓存查找, 因为cache传入的为NO, 这里不会进行缓存查找, 因为在汇编语言中CacheLookup已经查找过
// Optimistic cache lookup
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}

// runtimeLock is held during isRealized and isInitialized checking
// to prevent races against concurrent realization.

// runtimeLock is held during method search to make
// method-lookup + cache-fill atomic with respect to method addition.
// Otherwise, a category could be added but ignored indefinitely because
// the cache was re-filled with the old value after the cache flush on
// behalf of the category.

runtimeLock.lock();
checkIsKnownClass(cls);

if (!cls->isRealized()) {
realizeClass(cls);
}

if (initialize && !cls->isInitialized()) {
runtimeLock.unlock();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.lock();
// If sel == initialize, _class_initialize will send +initialize and
// then the messenger will send +initialize again after this
// procedure finishes. Of course, if this is not being called
// from the messenger then it won't happen. 2778172
}


retry:
runtimeLock.assertLocked();

// Try this class's cache.

// 防止动态添加方法,缓存会变化,再次查找缓存。
imp = cache_getImp(cls, sel);

// 如果查找到imp, 直接调用done, 返回方法地址
if (imp) goto done;

// 查找方法列表, 传入类对象和方法名
// Try this class's method lists.
{
// 根据sel去类对象里面查找方法
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
// 如果方法存在,则缓存方法
log_and_fill_cache(cls, meth->imp, sel, inst, cls);

// 方法缓存之后, 取出imp, 调用done返回imp
imp = meth->imp;
goto done;
}
}

// 如果类方法列表中没有找到, 则去父类的缓存中或方法列表中查找方法
// Try superclass caches and method lists.
{
unsigned attempts = unreasonableClassCount();
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
{
// Halt if there is a cycle in the superclass chain.
if (--attempts == 0) {
_objc_fatal("Memory corruption in class list.");
}

// 查找父类的缓存
// Superclass cache.
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
// 在父类中找到方法, 在本类中缓存方法, 注意这里传入的是cls, 将方法缓存在本类缓存列表中, 而非父类中
// Found the method in a superclass. Cache it in this class.
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
else {
// Found a forward:: entry in a superclass.
// Stop searching, but don't cache yet; call method
// resolver for this class first.
break;
}
}

// 查找父类的方法列表
// Superclass method list.
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
// 同样拿到方法, 在本类进行缓存
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
}

// ---------------- 消息发送阶段完成 ---------------------

// ---------------- 进入动态解析阶段 ---------------------


// 上述列表中都没有找到方法实现, 则尝试解析方法
// No implementation found. Try method resolver once.

if (resolver && !triedResolver) {
runtimeLock.unlock();
_class_resolveMethod(cls, sel, inst);
runtimeLock.lock();
// Don't cache the result; we don't hold the lock so it may have
// changed already. Re-do the search from scratch instead.
triedResolver = YES;
goto retry;
}

// ---------------- 动态解析阶段完成 ---------------------

// ---------------- 进入消息转发阶段 ---------------------

// No implementation found, and method resolver didn't help.
// Use forwarding.

imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);

done:
runtimeLock.unlock();

return imp;
}

根据上方源码的解析,得到消息发送阶段的流程如图:

动态解析阶段

当消息发送阶段没有找到方法实现的时候,就会进入动态方法解析阶段。我们来看一下动态解析阶段源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
if (resolver  &&  !triedResolver) {
runtimeLock.unlock();
_class_resolveMethod(cls, sel, inst);
runtimeLock.lock();
// Don't cache the result; we don't hold the lock so it may have
// changed already. Re-do the search from scratch instead.
triedResolver = YES;
goto retry;
}

void _class_resolveMethod(Class cls, SEL sel, id inst)
{
if (! cls->isMetaClass()) {
// try [cls resolveInstanceMethod:sel]
_class_resolveInstanceMethod(cls, sel, inst);
}
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
_class_resolveClassMethod(cls, sel, inst);
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
_class_resolveInstanceMethod(cls, sel, inst);
}
}
}

上述代码中可以发现,动态解析方法之后,会将triedResolver = YES;那么下次就不会在进行动态解析阶段了,之后会重新执行retry,会重新对方法查找一遍。也就是说无论我们是否实现动态解析方法,无论动态解析方法是否成功,retry之后都不会在进行动态的解析方法了。

  • 对象方法

动态解析对象方法时,会调用 _class_resolveInstanceMethod(cls, sel, inst) 方法。对应的 Objective-C 的方法是 +(BOOL)resolveInstanceMethod:(SEL)sel。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//执行foo函数
[self performSelector:@selector(foo:)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(foo:)) {//如果是执行foo函数,就动态解析,指定新的IMP
class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}

void fooMethod(id obj, SEL _cmd) {
NSLog(@"Doing foo");//新的foo函数
}
  • 类方法

动态解析类方法时,会调用 _class_resolveClassMethod(cls, sel, inst) 方法,对应的 Objective-C 的方法是 +(BOOL)resolveClassMethod:(SEL)sel。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@implementation Person

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//执行foo函数
[Person foo];
}

+ (BOOL)resolveClassMethod:(SEL)sel
{
if (sel == @selector(foo)) {
// 第一个参数是object_getClass(self),传入元类对象。
class_addMethod(object_getClass(self), sel, (IMP)fooMethod, "v16@0:8");
return YES;
}
return [super resolveClassMethod:sel];
}

void fooMethod(id obj, SEL _cmd) {
NSLog(@"Doing foo");//新的foo函数
}

上面的代码中,当动态解析方法时,我们动态的添加了方法的实现,这里引入了一个函数 class_addMethod,这个函数就是动态配置类时的关键函数之一。

我们看一下这个函数的声明:

1
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);

cls: 给哪个类添加方法。

name: 需要添加的方法名。 Objective-C 中可以直接使用 @selector(methodName) 得到方法名, Swift 中使用 #Selector(methodName)。

imp: 方法的实现,函数入口,函数名可与方法名不同(建议与方法名相同)。函数必须至少两个参数—— self 和 _cmd。

types: 参数以及返回值类型的字符串,需要用特定符号,参考官方文档Type encodings

我们从整个动态解析的过程可以看到,无论我们是否实现了动态解析的方法,系统内部都会执行 retry 对方法再次进行查找。那么如果我们实现了动态解析方法,此时就会顺利查找到方法,进而返回 imp 对方法进行调用。如果我们没有实现动态解析方法。就会进行消息转发。

消息转发阶段

当本类没有实现方法,并且没有动态解析方法,Runtime 这时就会调用 forwardingTargetForSelector 函数,进行消息转发,我们可以实现forwardingTargetForSelector 函数,在其内部将消息转发给可以实现此方法的对象。

实现一个完整转发的例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#import "Car.h"
@implementation Car
- (void) driving
{
NSLog(@"car driving");
}
@end

--------------

#import "Person.h"
#import <objc/runtime.h>
#import "Car.h"
@implementation Person

+ (BOOL)resolveInstanceMethod:(SEL)sel {
return YES;//返回YES,进入下一步转发
}

- (id)forwardingTargetForSelector:(SEL)aSelector
{
// 返回能够处理消息的对象
if (aSelector == @selector(driving)) {
return [[Car alloc] init];
}
return [super forwardingTargetForSelector:aSelector];
}
@end

--------------

#import<Foundation/Foundation.h>
#import "Person.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {

Person *person = [[Person alloc] init];
[person driving];
}
return 0;
}

// 打印内容
// 消息转发 car driving

如果 forwardingTargetForSelector 函数返回为 nil 或者没有实现的话,就会调用methodSignatureForSelector方法,用来返回一个方法签名,这也是我们正确跳转方法的最后机会。

如果 methodSignatureForSelector 方法返回正确的方法签名就会调用 forwardInvocation 方法,forwardInvocation 方法内提供一个 NSInvocation 类型的参数,NSInvocation 封装了一个方法的调用,包括方法的调用者,方法名,以及方法的参数。在 forwardInvocation 函数内修改方法调用对象即可。

如果 methodSignatureForSelector 返回的为 nil,就会来到 doseNotRecognizeSelector: 方法内部,程序 crash 提示无法识别选择器 unrecognized selector sent to instance。

代码验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#import "Car.h"
@implementation Car
- (void) driving
{
NSLog(@"car driving");
}
@end

--------------

#import<Foundation/Foundation.h>
#import "Person.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {

Person *person = [[Person alloc] init];
[person driving];
}
return 0;
}

--------------

#import "Person.h"
#import <objc/runtime.h>
#import "Car.h"
@implementation Person

+ (BOOL)resolveInstanceMethod:(SEL)sel {
return YES;//返回YES,进入下一步转发
}

- (id)forwardingTargetForSelector:(SEL)aSelector
{
// 返回能够处理消息的对象
if (aSelector == @selector(driving)) {
// 返回nil则会调用methodSignatureForSelector方法
return nil;
// return [[Car alloc] init];
}
return [super forwardingTargetForSelector:aSelector];
}

// 方法签名:返回值类型、参数类型
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
if (aSelector == @selector(driving)) {
// 通过调用Car的methodSignatureForSelector方法得到方法签名,这种方式需要car对象有aSelector方法
return [[[Car alloc] init] methodSignatureForSelector: aSelector];

}
return [super methodSignatureForSelector:aSelector];
}

/*
* NSInvocation 封装了一个方法调用,包括:方法调用者,方法,方法的参数
* anInvocation.target 方法调用者
* anInvocation.selector 方法名
* [anInvocation getArgument: NULL atIndex: 0]; 获得参数
*/
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
// anInvocation中封装了methodSignatureForSelector函数中返回的方法。
// 此时anInvocation.target 还是person对象,我们需要修改target为可以执行方法的方法调用者。
// anInvocation.target = [[Car alloc] init];
// [anInvocation invoke];
[anInvocation invokeWithTarget: [[Car alloc] init]];
}
@end

// 打印内容
// 消息转发 car driving

类方法消息转发同对象方法一样,同样需要经过消息发送,动态方法解析之后才会进行消息转发机制。需要注意的是类方法的接受者为类对象。其他同对象方法消息转发模式相同。

当类对象进行消息转发时,对调用相应的 + 号的 forwardingTargetForSelector、methodSignatureForSelector、forwardInvocation 方法。