前言:最近一直在研读《Effective Objective-C 2.0》这篇文章,觉得受益匪浅。自己将书上的代码进行实现,再结合作者的解释进行思路扩展之后,对oc语言机制有了更深的理解;
这篇文章结合了自身的理解,尽量更清楚明白的阐述消息转发机制的实现过程和原理,如有阐述不当的地方,欢迎指导改正。
什么是消息转发?
oc中方法调用就是一个消息传递的过程。例如:
[self testMethodWithInfo:info];
本例中,self被称为“接收者”,testMethodWithInfo:被称为选择器,info为参数,选择器和参数合起来称为“消息”。
但是,在编译期像对象发送了其无法解读的消息之后,编译器并不会报错,只会给出一个方法名未知的警告:
这是因为在运行期可以继续向类中添加方法,所以编译器在编译时还无法确定类中到底会不会有某个方法实现。当对象接收到无法解读的消息后,就会启动“消息转发”机制,我们也可以经由此过程老告诉对象应该如何处理未知消息。
消息转发机制的实现过程:
消息转发分为两大阶段。第一个阶段先征询接收者所属的类,看其是否能动态添加方法,以处理当前这个“未知的选择器”,这叫做“动态方法解析”。第二个阶段涉及“完整的消息转发机制”。如果运行期征询结果接收者没能动态添加方法以响应包含该选择器的消息,此时系统会请求接收者以其他手段来处理与消息相关的方法调用。首先,请接收者看看有没有其他对象能处理这条消息,如果有,系统会把消息转发给那个对象,消息转发过程结束。如果没有“备援的接受者”,则启动完整的消息转发机制,系统会把与消息相关的全部细节都封装在NSInvocation对象中,再给接收者最后一次机会,令其设法解决当前还未处理的这条消息。
动态方法解析:
对象在收到无法解读的消息后,首先会调用所属类的下列类方法:
+ (BOOL)resolveInstanceMethod:(SEL)sel
参数sel就是未知的选择器,返回值BOOL类型,表示这个类能否新增一个实例方法以处理此选择器。假如将要实现的不是实例方法而是类方法,系统则会调用另外一个方法:
+ (BOOL)resolveClassMethod:(SEL)sel
使用这种办法的前提是:相关方法的实现代码已经写好(否则识别不到新添加的方法名),只等着运行的时候动态插入到类里面就可以了。
演示代码:
先实现替代方法:
#import "StudentModel.h"
void nullMethodSubstite(id self, SEL _cmd){
NSLog(@"message has transpond");
};
然后在+ (BOOL)resolveInstanceMethod:(SEL)sel方法中进行处理:
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSString *str = NSStringFromSelector(sel);
if ([str containsString:@"nullMethod"]) {
/**
通过给定名称向类中添加新方法
@param self 指定类
@param sel 待处理的选择器
@param IMP 方法名
@return 用来描述方法参数类型的字符集
*/
class_addMethod(self, sel, (IMP)nullMethodSubstite, "@@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
1、获取选择器;2、检测选择器是否表示nullMethod方法;3、向类中添加该方法
查看运行结果:
备援接收者:
当前接收者还有第二次机会能处理未知的选择器,在该步骤中,系统会征询接收者能不能把这条消息转给其他接收者来处理。对应的处理方法如下:
- (id)forwardingTargetForSelector:(SEL)aSelector
实现代码:
@interface ModelPrintViewController ()
{
//新建Student类
StudentModel *_model;
}
@end
- (id)forwardingTargetForSelector:(SEL)aSelector{
if ([NSStringFromSelector(aSelector) containsString:@"nullMethod"]) {
return _model;
}
return [super forwardingTargetForSelector:aSelector];
}
在student类中实现代码:
- (void)nullMethod{
NSLog(@"This is Student nullMethod");
}
1、判断是否为nullMethod方法;2、设置备援接收者;3、在备援接收者中实现该代码
查看运行结果:
注:我们无法操作经由这一步转发的消息,只能设置备援接收者替代当前对象来接收这一消息,如果想在发送给备援接收者之前先修改消息内容,那就得通过完整的消息转发机制来做了。
完整的消息转发:
如果转发算法已经来到这一步的话,那么唯一能做的就是启用完整的消息转发机制了。首先创建NSInvocation对象,把与尚未处理的那条消息相关的全部细节都封于其中。此对象包含选择器、目标(target)、及参数。在触发NSInvocation对象时,“消息派发系统将亲自出马,把消息指派给目标对象”。
此步骤会调用下列方法来转发消息:
- (void)forwardInvocation:(NSInvocation *)anInvocation
这个方法的实现可以很简单:只需改变调用目标,使消息在新目标上得以调用即可。然而这样实现出来的方法与“备援接收者“方案所实现的方法等效,所以很少有人采用这么简单的实现方式。比较有用的实现方式为:在触发消息前,先以某种方式改变消息内容,比如追加另外一个参数,或者改换选择器,等等。
实现此方法,若发现某调用操作不应由本类处理,则需调用超累的同名方法,这样的话,继承体系中的每个类都有机会处理此调用请求,直至NSObject。如果最后调用了NSObject类的方法,那么该方法还会调用”doesNotRecognizeSelector:“,以抛出异常,表明选择器最终未能得到处理。如图:
消息转发全流程:
上图描述了消息转发机制处理消息的各个步骤。
以完整的例子演示动态方法解析:
@dynamic: 使用@dynamic关键字声明属性,可以让编译器默认不去自动创建实现属性所用的实例变量,也不会为其创建存取方法,并且编译器不会报错。
为了说明消息转发机制的意义,下面示范如何以动态方法解析来实现@dynamic属性。
创建student对象,并且将其属性用@dynamic关键字进行声明后,在不创建实例变量和存取方法的情况下,通过消息转发机制实现其存取方法。示例如下:
#import <Foundation/Foundation.h>
@interface StudentModel : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *city;
@property (nonatomic, copy) NSString *country;
@end
#import "StudentModel.h"
@interface StudentModel ()
//muDict用来传值
@property (nonatomic, strong) NSMutableDictionary *muDict;
@end
@implementation StudentModel
@dynamic name```,city,country;
进行此操作后,编译器不会自动创建实例变量和存取方法,此时用点语法进行属性的存取值时,运行期系统找不到对应的选择器,此时消息转发机制启动,系统会调用所属类的+ (BOOL)resolveInstanceMethod:(SEL)sel方法,我们可以在此方法中对选择器进行处理:
首先实现替代方法:
id autoStudentGetter(id self, SEL _cmd){
StudentModel *model = (StudentModel *)self;
NSMutableDictionary *dict = model.muDict;
NSString *key = NSStringFromSelector(_cmd);
NSString *str = [[key substringFromIndex:3]lowercaseString];
return [dict objectForKey:str];
}
void autoStudentSetter(id self, SEL _cmd, id value){
StudentModel *model = (StudentModel *)self;
NSMutableDictionary *dict = model.muDict;
NSString *str = NSStringFromSelector(_cmd);
NSMutableString *key = [str mutableCopy];
[key deleteCharactersInRange:NSMakeRange(key.length-1, 1)];
[key deleteCharactersInRange:NSMakeRange(0, 3)];
NSString *lowerCharStr = [key lowercaseString];
if (value && ([value isKindOfClass:[NSString class]] || [value isKindOfClass:[NSArray class]] || [value isKindOfClass:[NSNumber class]])) {
[dict setObject:value forKey:lowerCharStr];
}else{
[dict removeObjectForKey:lowerCharStr];
}
}
然后在+ (BOOL)resolveInstanceMethod:(SEL)sel方法中进行逻辑判断处理:
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSString *str = NSStringFromSelector(sel);
if ([str containsString:@"set"]) {
class_addMethod(self, sel, (IMP)autoStudentSetter, "v@:@");
}else{
class_addMethod(self, sel, (IMP)autoStudentGetter, "@@:");
}
return YES;
}
使用点语法给对象进行赋值,然后进行打印:
- (void)setData{
_model = [[StudentModel alloc]init];
_model.name = @"Samson";
_model.city = @"FuYang";
_model.country = @"China";
NSLog(@"model.name:%@,model.city:%@,model.country:%@",_model.name,_model.city,_model.country);
}
查看打印结果: