0%

Objective-C Runtime 之三 理解消息机制

ObjC 中的方法调用是一个传递消息的过程,它不是直接去调用一个函数,而是通过 selector 去查找 IMP 的过程,最终查找到函数指针,然后执行具体函数实现;如果没有找到函数实现,还会继续执行消息转发流程,最后仍没有查找到函数实现,就报错崩溃。

objc_msgSend 函数

1
2
3
4
5
// message.h

OBJC_EXPORT id _Nullable
objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

ObjC 通常的方法调用实质上就是调用 objc_msgSend 函数。

如下代码:

1
2
Person *olivia = [[Person alloc] init];
[olivia sayHi];

使用命令 clang -rewrite-objc main.m 编译成 C++ 代码后如下:

1
2
Person *olivia = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)olivia, sel_registerName("sayHi"));

如果是使用 super 关键字,则调用的是 objc_msgSendSuper 函数。

消息发送流程

  1. 检查 selector 是否要忽略,比如自动引用计数下 retainrelease 方法将会被忽略。
  2. 检查 target 是否为 nil,如果为 nil 则直接返回 nil
  3. 通过 selector 查找具体的 IMP,首先查找缓存列表。具体方法就是通过一个哈希函数将 SEL 映射到指定的 bucket_t 在数组中的位置。
  4. 如果换成列表没有,开始查找方法列表。对于已排序好的列表,采用 二分查找法 进行查找。对于没有排序好的列表,就进行遍历查找。如果查找到了会将对应的 IMP 放入方法缓存列表中。
  5. 如果方法列表没有,逐级查找父类缓存列表和方法列表,最终直到查找 NSObject 类。
  6. 进入动态方法解析、消息转发流程。

消息转发

动态方法解析

如果给一个对象发送消息,该对象并没有对应的方法实现,那么程序不会直接崩溃,首先会进入动态方法解析的过程。

1
2
3
4
// NSObject.h

+ (BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
+ (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

动态方法解析核心是上述两个方法(都是 NSObject 的类方法),分别解决类方法和实例方法。当对象没有对应方法实现的时候,上述方法就是第一个补救措施。

对于 @dynamic 关键字到属性,系统不提供 settergetter 方法实现,就需要在动态方法解析中为其动态添加两个方法实现。

动态添加方法

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
void playIMP(id self, SEL _cmd) {
NSLog(@"Game Start!");
}

void playWithTypeIMP(id self, SEL _cmd, GameType type) {
switch (type) {
case GameTypeOne:
NSLog(@"Game Type One Start!");
break;
case GameTypeTwo:
NSLog(@"Game Type Two Start!");
break;
case GameTypeThree:
NSLog(@"Game Type Three Start!");
break;
default:
break;
}
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(play)) {
class_addMethod(self, sel, (IMP)playIMP, "v@:");
return YES;
} else if (sel == @selector(playWith:)) {
class_addMethod(self, sel, (IMP)playWithTypeIMP, "v@:i");
return YES;
}

return [super resolveInstanceMethod:sel];
}

resolveInstanceMethod 方法中,使用 class_addMethod 函数为指定的类动态添加方法。

消息重定向

如果 resolveInstanceMethod: 方法中没有找到方法实现,将会进入第二个补救措施,就是消息重定向 forwardingTargetForSelector:,这个方法可以将消息的接受者更换为其他对象。

1
2
3
4
5
6
7
8
- (id)forwardingTargetForSelector:(SEL)aSelector {

if (aSelector == @selector(play)) {
return [SecondGame new]; // 返回一个其他的对象,如果这个对象存在这个方法实现的话
}

return nil;
}

转发

如果上述方法返回 nil,则会进入到最后一个补救方法中,即消息转发流程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(play)) {

// 这里返回方法签名
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
// 上面是直接写,下面是返回一个方法的 types,都可以
// return [self methodSignatureForSelector:@selector(anotherPlay)];
}

return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {

// 使用另一个方法实现做代替
if (anInvocation.selector == @selector(play)) {
anInvocation.selector = @selector(anotherPlay);
// 也可以是其他类,看实际情况。
[anInvocation invokeWithTarget:self];
}
}

最终这里可以实现,让原本的消息接受者去执行一个其他的方法,也可以更好消息接受者,去执行一个指定的方法,都是可以的。

模拟继承

使用消息转发的原理,可以实现模拟多继承,我们定义一个 Person 类和一个 Machine 类,并定义一个 Robot 类对前两个类进行继承。

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
// Person

@interface Person : NSObject

- (void)hello;

@end
@implementation Person

- (void)hello {
NSLog(@"Hello World!");
}

@end

// Machine

@interface Machine : NSObject

- (void)lubricate;

@end
@implementation Machine

- (void)lubricate {
NSLog(@"Lubricate.");
}

// Robot
@interface Robot : NSObject

- (void)hello;
- (void)lubricate;

@end

#import "Robot.h"
#import "Person.h"
#import "Machine.h"

@interface Robot ()

@property (nonatomic, strong) Person *person;
@property (nonatomic, strong) Machine *machine;

@end

@implementation Robot

- (instancetype)init {
self = [super init];

if (self) {
self.person = [[Person alloc] init];
self.machine = [[Machine alloc] init];
}

return self;
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
if ([self.person respondsToSelector:aSelector]) {
return self.person;
} else if ([self.machine respondsToSelector:aSelector]) {
return self.machine;
} else {
return [super forwardingTargetForSelector:aSelector];
}
}

@end