在Objective-C中,使用对象进行方法调用是一个消息发送的过程(Objective-C采用“动态绑定机制”,所以所要调用的方法直到运行期才能确定)。
方法在调用时,系统会查看这个对象能否接收这个消息(查看这个类有没有这个方法,或者有没有实现这个方法。),如果不能并且只在不能的情况下,就会调用下面这几个方法,给你“补救”的机会,你可以先理解为几套防止程序crash的备选方案,我们就是利用这几个方案进行消息转发,注意一点,前一套方案实现后一套方法就不会执行。如果这几套方案你都没有做处理,那么程序就会报错crash。
OC的运行时在程序崩溃前提供了三次拯救程序的机会:
方案一:
1
|
+ (
BOOL
)resolveInstanceMethod:(SEL)sel
|
1
|
+ (
BOOL
)resolveClassMethod:(SEL)sel
|
方案二:
1
|
- (id)forwardingTargetForSelector:(SEL)aSelector
|
方案三:
1
|
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
|
1
|
- (
void
)forwardInvocation:(NSInvocation *)anInvocation;
|
上图显示了消息转发的具体流程,接收者在每一步中均有机会处理消息。步骤越往后处理消息的代价越大。首先,会调用
+ (BOOL)resolveInstanceMethod:(SEL)sel。若方法返回YES,则表示可以处理该消息。在这个过程,可以动态地给消息增加方法。
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
|
// Person.m
// 不自动生成getter和setter方法
@dynamic name;
+ (
BOOL
)resolveInstanceMethod:(SEL)sel
{
if
(sel == @selector(name)) {
// BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
class_addMethod(self, sel, (IMP)GetterName,
"@@:"
);
return
YES;
}
if
(sel == @selector(setName:)) {
class_addMethod(self, sel, (IMP)SetterName,
"v@:@"
);
return
YES;
}
return
[super resolveInstanceMethod:sel];
}
// (用于类方法)
//+ (BOOL)resolveClassMethod:(SEL)sel
//{
// NSLog(@"resolveClassMethod called %@", NSStringFromSelector(sel));
//
// return [super resolveClassMethod:sel];
//}
id GetterName(id self, SEL cmd)
{
NSLog(@
"%@, %s"
, [self
class
], sel_getName(cmd));
return
@
"Getter called"
;
}
void
SetterName(id self, SEL cmd, NSString *value)
{
NSLog(@
"%@, %s, %@"
, [self
class
], sel_getName(cmd), value);
NSLog(@
"SetterName called"
);
|
签名符号含义:
* 代表 char * char BOOL 代表 c : 代表 SEL ^type 代表 type * @ 代表 NSObject * 或 id ^@ 代表 NSError ** # 代表 NSObject v 代表 void
1
2
3
4
5
6
7
8
9
|
// main.m
/* 现在在main.m中给Person发送setName:和name消息,由于Person中未实现这两个方法,就会经消息转发调用GetterName和SetterName方法
*/
Person *person = [[Person alloc] init];
[person setName:@
"Jake"
];
NSLog(@
"%@"
, [person name]);
|
1
2
3
4
5
6
|
// 输出结果:
Person, setName:, Jake
SetterName called
Person, name
Getter called
|
若方法返回NO,则进行消息转发的第二步,查找是否有其它的接收者。对应的处理函数是:
- (id)forwardingTargetForSelector:(SEL)aSelector。可以通过该函数返回一个可以处理该消息的对象。
现在新建一个类Child,在Child中实现一个eat方法,在Person类中定义eat方法但不实现它。
1
2
3
4
5
6
|
// Child.m
- (
void
)eat
{
NSLog(@
"Child method eat called"
);
}
|
然后在Person类中实现forwardingTargetForSelector:方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// Person.m
// 当调用Person中的eat方法时,由于Person中并未实现该方法,就会经下面的方法将消息转发给可以处理eat方法的对象
- (id)forwardingTargetForSelector:(SEL)aSelector
{
NSString *selStr = NSStringFromSelector(aSelector);
if
([selStr isEqualToString:@
"eat"
]) {
return
[[Child alloc] init];
// 这里返回Child类对象,让Child去处理eat消息
}
return
[super forwardingTargetForSelector:aSelector];
}
|
1
2
3
|
// main.m
[person eat];
|
1
2
3
|
// 输出结果:
Child method eat called
|
通过此方案,我们可以用“组合”来模拟出“多重继承”的某些特性。在一个对象内部,可能还有一系列其他对象,该对象可以经由此方法将能够处理某选择子的相关内部对象返回,这样的话,在外界看来好像是该对象亲自处理了这些消息。
伪多继承与真正的多继承的区别在于,真正的多继承是将多个类的功能组合到一个对象中,而消息转发实现的伪多继承,对应的功能仍然分布在多个对象中,但是将多个对象的区别对消息发送者透明。
若第二步返回nil,则进入消息转发的第三步。调用
- (void)forwardInvocation:(NSInvocation *)anInvocation。这个方法实现得很简单。只需要改变调用目标,使消息在新目标上得以调用即可。不过,如果采用这种方式,实现的效果与第二步的消息转发是一致的。所以比较有用的实现方式是:先以某种方式改变消息内容,比如追加另外一个参数,或者改换选择子,等等。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
// Person.m
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
NSString *sel = NSStringFromSelector(aSelector);
// 判断要转发的SEL
if
([sel isEqualToString:@
"sleep"
]) {
// 为转发的方法手动生成签名
return
[NSMethodSignature signatureWithObjCTypes:
"v@:"
];
}
return
[super methodSignatureForSelector:aSelector];
}
- (
void
)forwardInvocation:(NSInvocation *)anInvocation
{
SEL selector = [anInvocation selector];
// 新建需要转发消息的对象
Child *child = [[Child alloc] init];
if
([child respondsToSelector:selector]) {
// 唤醒这个方法
[anInvocation invokeWithTarget:child];
}
}
|
1
2
3
4
5
6
7
8
9
10
11
|
// Child.h
#import <Foundation/Foundation.h>
@interface Child : NSObject
- (
void
)eat;
- (
void
)sleep;
@end
|
1
2
3
4
5
6
|
// Child.m
- (
void
)sleep
{
NSLog(@
"Child method sleep called"
);
}
|
1
2
3
|
// 输出结果:
Child method sleep called
|
有时候服务器很烦不靠谱,老是不经意间返回null,可以重写NSNull的消息转发方法, 让他能处理这些异常的方法,达到解决问题的目的。