在Objective-C中,使用对象进行方法调用是一个消息发送的过程(Objective-C采用“动态绑定机制”,所以所要调用的方法直到运行期才能确定)。
方法在调用时,系统会查看这个对象能否接收这个消息(查看这个类有没有这个方法,或者有没有实现这个方法。),如果不能并且只在不能的情况下,就会调用下面这几个方法,给你“补救”的机会,你可以先理解为几套防止程序crash的备选方案,我们就是利用这几个方案进行消息转发,注意一点,前一套方案实现后一套方法就不会执行。如果这几套方案你都没有做处理,那么程序就会报错crash。
OC的运行时在程序崩溃前提供了三次拯救程序的机会:
方案一:
+ (BOOL)resolveInstanceMethod:(SEL)sel
+ (BOOL)resolveClassMethod:(SEL)sel
方案二:
- (id)forwardingTargetForSelector:(SEL)aSelector
方案三:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;
上图显示了消息转发的具体流程,接收者在每一步中均有机会处理消息。步骤越往后处理消息的代价越大。首先,会调用
+ (BOOL)resolveInstanceMethod:(SEL)sel。若方法返回YES,则表示可以处理该消息。在这个过程,可以动态地给消息增加方法。
// 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
// main.m
/* 现在在main.m中给Person发送setName:和name消息,由于Person中未实现这两个方法,就会经消息转发调用GetterName和SetterName方法
*/
Person *person = [[Person alloc] init];
[person setName:@"Jake"];
NSLog(@"%@", [person name]);
// 输出结果:
Person, setName:, Jake
SetterName called
Person, name
Getter called
// 若方法返回NO,则进行消息转发的第二步,查找是否有其它的接收者。对应的处理函数是:
- (id)forwardingTargetForSelector:(SEL)aSelector。可以通过该函数返回一个可以处理该消息的对象。
现在新建一个类Child,在Child中实现一个eat方法,在Person类中定义eat方法但不实现它。
// Child.m
- (void)eat
{
NSLog(@"Child method eat called");
}
然后在Person类中实现forwardingTargetForSelector:方法:
// 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];
}
// main.m
[person eat];
// 输出结果:
Child method eat called
通过此方案,我们可以用“组合”来模拟出“多重继承”的某些特性。在一个对象内部,可能还有一系列其他对象,该对象可以经由此方法将能够处理某选择子的相关内部对象返回,这样的话,在外界看来好像是该对象亲自处理了这些消息。
伪多继承与真正的多继承的区别在于,真正的多继承是将多个类的功能组合到一个对象中,而消息转发实现的伪多继承,对应的功能仍然分布在多个对象中,但是将多个对象的区别对消息发送者透明。
若第二步返回nil,则进入消息转发的第三步。调用
- (void)forwardInvocation:(NSInvocation *)anInvocation。这个方法实现得很简单。只需要改变调用目标,使消息在新目标上得以调用即可。不过,如果采用这种方式,实现的效果与第二步的消息转发是一致的。所以比较有用的实现方式是:先以某种方式改变消息内容,比如追加另外一个参数,或者改换选择子,等等。
// 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];
}
}
// Child.h
#import <Foundation/Foundation.h>
@interface Child : NSObject
- (void)eat;
- (void)sleep;
@end
// Child.m
- (void)sleep
{
NSLog(@"Child method sleep called");
}
// 输出结果:
Child method sleep called
有时候服务器很烦不靠谱,老是不经意间返回null,可以重写NSNull的消息转发方法, 让他能处理这些异常的方法,达到解决问题的目的。
之二
一 概述
在编译期向类发送了其无法解读的的消息并不会报错,因为在运行期可以继续让类中添加方法,所有编译器在编译时还无法确知类中到底会不会有某个方法实现,当对象接收到无法解读的消息后,就会启动 消息转发 机制,程序员可经由此过程告诉对象应该如何处理未知消息。
在程序运行中,有时会以下异常信息
1 | |
上段信息就是发送一个未识别的消息给实例,类定义但对象未实现function_name 方法
二 消息转发的两大阶段
第一阶段先征询接受者所属的类,看其是否能动态添加方法,以处理当前“未知的选择子”,叫做“动态方法解析”。
第二阶段涉及“完整的消息转发机制”,如果运行期系统已经把第一阶段执行完了,那么接受者自己就无法再以动态新增方法的手段来响应包含该选择子的消息了。此时,运行期系统会请求接受者以其他手段来处理与消息相关的调用方法。这又细分为两小阶段。首先,请接受者看看有没有其他对象能处理这条信息。若有,则运行期系统会把消息转给那个对象,于是消息转发过程结束。若没有“备援的接受者”,则启动完成的消息转发机制,运行期系统会把与消息有关的细节全部封装到NSInvocation对象中,让接受者最后一次设法解决当前还未处理的这条消息
2.1动态方法解析
对象接收到无法解读的消息后,首先将调用所属类的下列类方法:
1 | |
该方法的参数就是未知的选择子,返回类型为Boolean,表示类是否能新增一个实例方法用以处理此选择子。在继续往下执行转换机制之前,本类可用新增处理此选择子的方法,假如尚未实现的方法不是实例方法而是类方法,那么运行期系统就会调用另外一个方法,该方法与“resolveInstanceMethod:”类似,叫做”resolveClassMethod:” 。
使用这种办法的前提是:相关方法的代码实现已经写好,只等着运行的时候动态插在类里面就可以了。此方案常用来实现@dynamic 属性,比如要访问CoreData框架中NSManagedObjects对象的属性时就可用这么做,因为实现这些属性所需要的存取方法在编译器就能确定
2.2.1 备援接受者
备援接受者为处理未知的选择子提供第二次机会,嫩故能把这条消息转给其他接受者来处理。该步骤对应的处理方法如下:
1 | |
方法参数代码未知的选择子,若当前接受者能找到备援对象,则将其返回,若找不到,就返回nil。通过次方案,我们可以用“组合”来模拟“多重继承”的某些特性。在一个对象内部,可能还有一系列其他对象,该对象可经由此方法将能够处理某选择子的相关内部对象返回,从外界看来,好像是该对象亲自处理这些消息。若是想在发送给备援接受者之前先修改消息内容,那就通过完整消息转发机制来做。
2.2.2 完整的消息转发
如果转发算法到了这一步,那么唯一能做的就是启用完整的消息转发机制,首先创建NSInvocation对象,把尚未处理的那条消息有关的全部细节都封于其中。此对象包含选择子,目标及参数。在触发NSInvocation对象时,“消息派发系统”将会把消息指派给目标对象。
此步骤会调用下列方法来转发消息:
1 | |
该方法实现简单,只需要改变调用目标,使消息在新目标上得以调用即可。然而这样实现出来的方法与“备援接受者”方案所实现的方法等效,所有很少有人采用这么简单的实现方式。比较有用的实现方式为:在触发消息前,先以某种方式改变消息内容,比如追加另外一个参数,或是改变选择子。
实现该方法时,若发现某调用操作不应由本类处理,则需要调用超类的同名方法。这样集成体系中的某个类都有机会处理此方法调用,直到NSObject,继而调用”doesNotRecognizeSelector:”以抛出异常,此异常表明选择子最终未能得到处理。
三 消息转发全流程
接受者在每一步中均有机会处理信息。步骤越往后,处理消息的代价就越大,最好能在第一部就能处理完,这样,运行期系统就可以将方法缓存起来,如果这个类的实例稍后收到同名选择子,就武器启动消息转发流程。若想在第三部把消息转给备援接受者,还不如提前到第二步,因为第三步只是修改了调用目标,这项改动放在第二步执行更为简单,而且不用创建并处理完整的NSInvocation
四 总结
- 若对象无法响应某个选择子,则进入消息转发机制
- 通过运行期的 动态方法解析
- 对象可以把其无法解读的某些选择子转交给其他对象来处理(备援接受者\完整的消息转发)
- 经过上述两步之后,如果还是没办法处理选择子,那就启动完整的消息转发机制