iOS开发-runtime-消息传递和转发机制

objective-c是一门动态语言,动态语言的特点就是方法的调用是在运行时(runtime)而不是编译时。

C语言就是一门典型的静态语言,在编译时程序就已经知道要调用的函数,所以在C语言中如果某个函数没有实现,是没有办法编译成功的。

但是OC不同,在编译时并没有确定的知道要调用的函数,甚至于在运行时还可以动态的添加方法,但是这也带来了问题,就是只有在程序运行起来之后,我们才会发现某些方法没有实现,而这种情况往往导致了程序的崩溃。

1
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[ViewController btnClicked]: unrecognized selector sent to instance 0x7f9a3a40b640'

那接下来我们就来彻底的了解一下这门动态语言中的消息是如何传递和转发的。

objc_msgSend() 函数

说到消息传递机制,最应该说的就是大名鼎鼎的objc_msgSend()函数了。相信写过一段时间OC代码的同学们应该都有或多或少的听说过这个方法。

我们每天写的OC代码,在编译器看到时都会转换成一条C语言函数调用。

我们先来看一下objc_msgSend的结构是什么样的。

1
void objc_msgSend(id self, SEL cmd, ...)

咋一看好像有点懵,这都什么对什么呀。没关系,我们用OC代码带进去看一下就明白了。

比如在OC中的这个方法:

1
id returnValue = [someObject messageName:parameter];

在编译器中就变成了:

1
id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter)

这下是不是清楚很多啦,接下来我们再来看看每个参数都是什么意思,还是用上边的这个例子:

  • someObject为方法的调用者,也可以叫做接受者(receiver);
  • messageName:是方法名,也就是选择子(selector);
  • parameter是参数,参数可以是一个,也可以是多个;
  • messageName和参数合起来叫做消息(message);

了解了这些参数的含义之后,我们来看一下消息传递的流程。

消息传递流程

首先接受者根据isa指针找到接受者自己的类,每个类都会有自己的方法列表(method list),然后会在这个方法列表中遍历。

如果这时候找到合适的方法名,就会根据IMP指针跳转到方法的实现代码,调用方法的实现。

如果没有找到合适的方法名,接受者会根据所属类的父类(superClass)指针,在父类中遍历寻找方法名,如果父类没有,就去父类的父类寻找,一级一级向上。

如果在父类们中找到了合适的方法名,就会跳转到方法的实现代码,调用方法的实现。

如果还没有找到合适的方法,就会执行消息转发(message forwarding)

消息转发

消息转发其实我们已经见过了。

1
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[ViewController btnClicked]: unrecognized selector sent to instance 0x7f9a3a40b640'

之前说的这段代码就是消息转发给了NSObjectdoesNotRecognizeSelector:方法抛出来的,意思就是接受者ViewController无法响应btnClicked这个消息,最后转发给了最最最父类NSObject处理,而NSObject也无法处理,就抛出了异常。

消息转发流程

消息转发一共分为三个阶段

  • 1.第一阶段为动态方法解析(dynamic method resolution),就是看看能否动态添加方法处理当前无法解读的消息,如果此时无法动态新增方法,则进入第二阶段。
  • 2.第二阶段为备援接受者(replacement receiver),也就是看看运行时系统会请求当前接受者让其他接受者处理这条消息,如果还没有办法,则进入第三阶段。
  • 3.第三阶段为完整消息转发机制,运行时系统会把消息有关的细节全部封装到NSInvocation对象中,再给接受者最后一次机会,如果还没有办法解决,就只能抛出异常了。

动态方法解析

这是整个消息转发流程的第一个阶段,如果在收到无法响应的消息后,会调用所属类的方法:

1
2
//实例对象
+ (BOOL)resolveInstanceMethod:(SEL)selector
1
2
//类对象
+ (BOOL)resolveClassMethod:(SEL)selector

其中参数selector为未处理的方法。

返回值@return表示能否新增一个方法来处理,一般使用@dynamic属性来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/************** 使用 resolveInstanceMethod 实现 @dynamic 属性 **************/
id autoDictionaryGetter(id self, SEL _cmd);
void autoDictionarySetter(id self, SEL _cmd, id value);
+ (BOOL)resolveInstanceMethod:(SEL)selector
{
NSString *selectorString = NSStringFromSelector(selector);
if (/* selector is from a @dynamic property */)
{
if ([selectorString hasPrefix:@"set"])
{
// 添加 setter 方法
class_addMethod(self, selector, (IMP)autoDictionarySetter, "v@:@");
}
else
{
// 添加 getter 方法
class_addMethod(self, selector, (IMP)autoDictionaryGetter, "@@:");
}
return YES;
}
return [super resolveInstanceMethod:selector];
}

备援接受者

这是整个消息转发机制的第二站,看名字就可以看出来,这是在寻找一个备用援救的接受者,到了这一阶段,系统会调用这个方法:

1
- (id)forwardingTargetForSelector:(SEL)aSelector;

传入参数aSelector同样为无法处理的方法。

返回值为当前找到的备援接受者,如果没有则返回nil,进入下一阶段。

完整的消息转发机制

如果前两个阶段都没有办法处理消息,就会启动完整的消息转发机制。

首先会创建NSInvocation对象,把尚未处理的那条消息的全部信息细节装在里边,在触发NSInvocation对象时,系统派发系统(message-dispatch system)将会把消息指派给目标对象。这时会调用该方法:

1
- (void)forwardInvocation:(NSInvocation *)anInvocation;

传入的参数anInvocation就包含了消息的所有内容。

如果此时还是没办法处理消息,就会沿着继承的顺序一步一步向父类调用相同的方法,直到最后的NSObject类中,这时候如果还没有办法处理消息,就会调用doesNotRecognizeSelector:抛出异常,之前看到的崩溃原因就是这个了。

到此为止,消息转发的整个流程就都结束了。

所以,利用消息转发机制,在程序崩溃之前,我们还有三次机会来拯救我们的应用,避免崩溃的事件发生。

Demo

最后放个Demo吧,代码比较简单,就不放地址了,直接把代码贴上来。

EOCAutoDictionary.h

1
2
3
4
5
6
7
8
9
10
#import <Foundation/Foundation.h>
@interface EOCAutoDictionary : NSObject
@property (nonatomic ,strong) NSString *string;
@property (nonatomic ,strong) NSNumber *number;
@property (nonatomic ,strong) NSDate *date;
@property (nonatomic ,strong) id opaqueObject;
@end

EOCAutoDictionary.m

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
#import "EOCAutoDictionary.h"
#import <objc/runtime.h>
@interface EOCAutoDictionary ()
@property (nonatomic ,strong) NSMutableDictionary *backingStore;
@end
@implementation EOCAutoDictionary
@dynamic string, number, date, opaqueObject;
- (id)init
{
self = [super init];
if(self)
{
_backingStore = [[NSMutableDictionary alloc] init];
}
return self;
}
//这里注意实例方法和类方法
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
NSString *selectorStirng = NSStringFromSelector(sel);
if([selectorStirng hasPrefix:@"set"])
{
class_addMethod(self, sel, (IMP)autoDictionarySetter, "v@:@");
}
else
{
class_addMethod(self, sel, (IMP)autoDictionaryGetter, "@@:");
}
return YES;
}
id autoDictionaryGetter(id self, SEL _cmd)
{
EOCAutoDictionary *typeSelf = (EOCAutoDictionary *)self;
NSMutableDictionary *backingStore = typeSelf.backingStore;
NSString *key = NSStringFromSelector(_cmd);
return [backingStore objectForKey:key];
}
void autoDictionarySetter(id self ,SEL _cmd, id vaule)
{
EOCAutoDictionary *typeSelf = (EOCAutoDictionary *)self;
NSMutableDictionary *backingStore = typeSelf.backingStore;
//获取方法名
NSString *selectorString = NSStringFromSelector(_cmd);
NSMutableString *key = [selectorString mutableCopy];
//去除尾部“:”符号
[key deleteCharactersInRange:NSMakeRange(key.length-1, 1)];
//去除头部“set”
[key deleteCharactersInRange:NSMakeRange(0, 3)];
NSString *lowercaseFirstChar = [[key substringFromIndex:1] lowercaseString];
[key replaceCharactersInRange:NSMakeRange(0, 1) withString:lowercaseFirstChar];
if(vaule)
{
[backingStore setObject:vaule forKey:key];
}
else
{
[backingStore removeObjectForKey:key];
}
}

调用方法:

1
2
3
EOCAutoDictionary *dict = [[EOCAutoDictionary alloc] init];
dict.date = [NSDate dateWithTimeIntervalSince1970:475372800];
NSLog(@"dict.date = %@",dict.date);

Demo中使用了@dynamic,但是没有实现变量的setter和getter方法,然后在resolveInstanceMethod:方法中动态的添加了setter和getter方法。

此篇文章大部分内容来自于《Effective Objective-C 2.0》中的第11条和第12条,仅为作者学习使用,如果有什么地方写的不对,还请大佬们多多指教。