从消息转发到多继承

从消息转发到多继承

0x0 消息的动态派发

Objective-C 中函数的调用是在运行时确定的,编译期确定的函数调用和运行时的区别,举个例子:

定义两个函数,如下:

1
2
3
4
5
6
7
void foo() {
print("foo");
}

void bar() {
print("bar");
}

假如编译期确定的 foo() 的首地址为 TEXT 段的 0x20,bar() 的首地址为 0x30,则当函数调用由编译期决定时:

1
2
3
4
5
6
7
8
main() {
if (flag) {
foo(); // 指令 callq 0x20
} else {
bar(); // 指令 callq 0x30
}
return 0;
}

也就是函数的首地址会被硬编码进目标文件。而函数调用由运行时决定时:

1
2
3
4
5
6
7
8
9
10
main() {
void (*func)();
if (flag) {
func = foo;
} else {
func = bar;
}
func(); //根据运行时 flag 的值确定指令 callq 0x20 还是 0x30
return 0;
}

此时,函数的地址被存储在变量 func 中,而自动变量的值只有当程序运行起来才能确定。因此,函数的调用是在运行时确定的。

OC 类中的实例方法和类方法会在运行时以函数指针的形式加载进类对象和元类的方法列表(C++ 中的虚函数表)中,即方法列表中存储着指向函数首地址的变量(函数指针)以及该函数的 selector(字符串);当 [obj method] 时,运行时会使用 method: 这个 selector去类对象的方法列表中找到函数指针,然后调用。

这就是 OC 的动态消息派发的工作方式。

0x1 消息转发

如上文所示,我们可以通过在运行时修改变量(函数指针)的值来修改函数的调用,也可以通过增删改查方法列表来增删改查实例方法或者类方法,因为类对象和元类是“单例”,使得这些增删改查更容易实现。

[obj method] 时,如果此时的 - method: 不在方法列表中时,OC 会提供消息转发的机制,以避免 crash。

消息转发主要分为三步:

    1. + resolveInstanceMethod:+ resolveClassMethod:
1
2
3
4
5
6
7
8
+ (BOOL) resolveInstanceMethod:(SEL)aSEL
{
if (aSEL == @selector(resolveThisMethodDynamically)) {
class_addMethod([self class], aSEL, (IMP)dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:aSel];
}

此方法允许我们动态的向类对象或者元类中添加方法,即添加对应的 selector 和函数指针。如果返回 No 则进入下一步。

    1. 快速转发 - forwardingTargetForSelector:
1
2
3
- (id)forwardingTargetForSelector:(SEL)aSelector {
return [OtherTarget new];
}

此方法允许我们将找不到的方法转发给其他方法列表中具有该方法的类对象的实例。如果返回 nil 则进入下一步。

    1. 正常转发 - forwardInvocation:
1
2
3
4
5
6
7
8
9
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
SEL aSelector = [anInvocation selector];
Friend *friend = [Friend new];
if ([friend respondsToSelector:aSelector])
[anInvocation invokeWithTarget:friend];
else
[super forwardInvocation:invocation];
}

NSInvocation 是消息的对象形式,每一个 NSInvocation 实例都存储了这个消息的 selector、target、参数和返回值,它用于在对象间和应用程序间存储和转发 message。因此当某个方法需要转发的时候,就会创建一个 NSInvocation 实例,将该方法的信息存储进去,以便转发的时候获取这些信息。

invocation 实例必须使用固定的 type encode 创建,也就是使用的时候必须要先确定某个方法的 type encode (method signature),因此重写该方法之前必须先重写 + methodSignatureForSelector: 以获取该方法的 type encode,比如:@@:i 表示该方法的返回值是 Object(@),接受者是 Object(@),: 代表 SEL,具有一个 int 类型的参数(i)。

0x2 多继承

多继承和消息转发有什么关系呢?

从上文可知,消息转发可以使得某个对象和其他对象之间建立某种联系,我们知道,类表述的某一类对象具有的共有的特征(属性或成员变量)和行为(方法),继承关系是子类对父类的特征和行为的扩展,消息转发使得某一个对象具有其他对象的特征和行为。

比如,一个 儿子 既想继承 父亲 写代码 的能力,又想继承 母亲 跳舞 的能力,在 OC 正常的语法中,我们知道只支持单继承,消息转发可以帮助我们实现:

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
// Son.h
// Son 的头文件定义这些方法,但是不实现,为了处理编译不通过
// 也可以不定义,使用 `performSelector`调用
@interface Son : NSObject
- (NSString *)writeCode;
- (void)dance;
@end

// Son.m
@implementation Son {
Mother *_mother;
Father *_father;
}

// 初始化被转发的对象
- (instancetype)init {
if (self = [super init]) {
_mother = [Mother new];
_father = [Father new];
}
return self;
}

// 获取 selector 的 type encode
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *s = [super methodSignatureForSelector:aSelector];

if ([_mother respondsToSelector:aSelector]) {
s = [_mother methodSignatureForSelector:aSelector];
} else if ([_father respondsToSelector:aSelector]) {
s = [_father methodSignatureForSelector:aSelector];
} else {
s = [super methodSignatureForSelector:aSelector];
}

return s;
}

// 转发
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL s = [anInvocation selector];
if ([_mother respondsToSelector:s]) {
[anInvocation invokeWithTarget:_mother];
} else if ([_father respondsToSelector:s]) {
[anInvocation invokeWithTarget:_father];
} else {
[super forwardInvocation:anInvocation];
}
}

// Mother.h
@interface Mother : NSObject
@end

// Mother.m
@implementation Mother
// 实现 dance 方法
- (void)dance {
NSLog(@"dance");
}
@end

// Father.h
@interface Father : NSObject
@end

// Father.m
@implementation Father
- (NSString *)writeCode {
return @"hello world";
}
@end

// main.m
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
Son *son = [Son new];
[son dance];
NSString *code = [son writeCode];
NSLog(@"%@", code);
}
return 0;
}

当执行 main.m 中的代码时,得到的结果为:

1
2
2019-10-14 15:37:38.525737+0800 Fizz[61993:5201679] dance
2019-10-14 15:37:38.526253+0800 Fizz[61993:5201679] hello world

因此,Son 得到了 Father 写代码的能力和 Mother 跳舞的能力。

那么此时的 Son 是得到了多个父类的能力(方法),那么假如此时 Son 想继承 FatherfirstName 呢?

我们知道,OC 中定义的属性会自动合成为 ivar + setter + getter 的形式,即生成成员变量、设置方法和获取方法,因此我们可以通过转发 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
// Father.h
@interface Father : NSObject
@property (nonatomic, copy) NSString *firstName;
@end

// Son.h
@interface Son : NSObject
// 定义 setter 和 getter
- (void)setFirstName:(NSString *)name;
- (NSString *)firstName;
- (NSString *)writeCode;
- (void)dance;
@end

// main.m
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
Son *son = [Son new];
[son dance];
NSString *tmp = [son writeCode];
NSLog(@"%@", tmp);
[son setFirstName:@"user"];
NSLog(@"%@", [son firstName]);
}
return 0;
}

此时的 log 为:

1
2
3
2019-10-14 16:01:53.229544+0800 Fizz[62378:5213867] dance
2019-10-14 16:01:53.229918+0800 Fizz[62378:5213867] hello world
2019-10-14 16:01:53.229971+0800 Fizz[62378:5213867] user

这样 Son 即可同时获取 FatherMother 的特征(仅属性,成员变量不行,实际上也是方法)和能力,实现多继承。

同时继承自 SonGrandson 也同时拥有 Son 多继承得到的属性和方法,因为方法查找会沿着父类链查找到根类,这就是另一个话题了。