引言:OC是一种消息语言,OC对象调用方法,就是给对象发送消息,这个过程称为消息传递,那如果对象接收到了无法解读的消息,这时候要怎么处理呢?此时就用到了OC中的消息转发机制(message forwarding)。本文分为两部分,第一部分介绍消息转发机制的过程,第二部分介绍消息转发机制的应用场景。
一.消息转发机制过程:
消息转发一共有三步:
1.动态方法解析(Dynamic Method Resolution):
+ (BOOL)resolveInstanceMethod:(SEL)selector; ①
+ (BOOL)resolveClassMethod:(SEL)selector;②
如果对象收到无法解读的消息,首先会调用对象所属类上述两个类方法之一,询问是否能够动态添加无法解读的selector。上述两个类方法分别对应selector为对象方法和类方法的情况。这两个方法返回值为BOOL,表示是否能新增一个方法来处理此选择子。
代码示例:
People*people = [[People alloc]init];
[people performSelector:NSSelectorFromString(@"tonightEatChicken")];
People类的实例对象people执行tonightEatChicken方法,而People类中并没有该方法的实现,如果不做任何处理,程序运行,将会崩溃。而如果我们使用动态方法解析做如下处理:
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSString *selString = NSStringFromSelector(sel);
if([selString isEqualToString:@"tonightEatChicken"]) {
//为当前类添加此方法
class_addMethod(self, sel , (IMP)tonightEatChicken, "v@:@");
returnYES;
}
return [super resolveInstanceMethod:sel];
}
void tonightEatChicken(id self,SEL_cmd) {
NSLog(@"%@--%@今晚吃鸡",self,NSStringFromSelector(_cmd));
}
再运行,程序正常运行,控制台输出<People: 0x6000023e06c0>--tonightEatChicken今晚吃鸡。
这是消息转发机制的第一步,值得注意的是:这一步骤中动态添加的方法,将会被运行时系统缓存,如果People类的实例稍后接收到同样的选择子,则不会进入消息转发流程,直接在消息发送阶段完成,这可以理解为runtime系统的优化工作,减少了方法查找的步骤。
2.备援接收者(Replacement Receiver)
如果当前接收者没有在第一步动态方法解析中进行处理,则还有第二次机会处理该selector,具体方法如下:
- (id)forwardingTargetForSelector:(SEL)selector;
这个方法同样由NSObject声明,所有继承于NSObject的类,都可以实现这个方法。这个方法需要返回可以接收该selector的类对象或者实例对象,如果该selector为类方法,则返回类对象,否则,返回实例。
具体实现如下,我们新建Soldier类并实现该选择子对象的方法:
- (id)forwardingTargetForSelector:(SEL)aSelector {
if ([NSStringFromSelector(aSelector) isEqualToString:@"tonightEatChicken"]) {
// //return [Soldier class];aSelector为类方法,则返回类对象
return [[Soldier alloc]init];//aSelector为实例方法,则返回类对象
}
return [super forwardingTargetForSelector:aSelector];
}
#import "Soldier.h"
@implementation Soldier
- (void)tonightEatChicken {
NSLog(@"士兵今晚吃鸡");
}
@end
程序成功运行并输出"士兵今晚吃鸡"。
其实在这一步,程序员能操作的就是改变消息的接收对象,这种方式可以模拟多重继承。OC是不支持多重继承的,利用消息转发可以变相的实现。外界看起来,似乎是一个类同时实现了两个类的某个功能,其实只是利用了消息转发。
3.完整的消息转发机制(Full Forwarding Mechanism)
如果前两步都没有处理,那么来到第三步,这一步系统会创建一个NSInvocation对象把这个消息的所有信息(包括target,selector,参数以及返回值)包装起来。并通过- (void)forwardInvocation:(NSInvocation *)anInvocation方法,把包装好的NSInvocation抛出来。
但是在创建NSInvocation对象之前,需要前获取这个消息的方法签名,通过
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if ([NSStringFromSelector(aSelector) isEqualToString:@"tonightEatChicken"]) {
return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
}
return [NSMethodSignature methodSignatureForSelector:aSelector];
}
然后再实现
- (void)forwardInvocation:(NSInvocation *)anInvocation {
[anInvocation invokeWithTarget:[[Soldier alloc]init]];
NSLog(@"%@",anInvocation);
}
如此,就会将此消息转发给Soldier,控制台会打印出“士兵今晚吃鸡”,实现和第二步一样的效果。
小结:
接收者在每一步均有机会处理消息,步骤越往后,处理消息的代价越大。最好能在第一步就处理完,这样的话,运行期系统就可以将此方法缓存起来,如果该类的实例稍后收到同名选择子,就无须启动消息转发流程。如果只是想改变消息的接收者,那么在第三步操作不如在第二步操作。相对于第二步,第三步还会创建并处理完整的NSInvocation。
二.应用场景:
了解了技术的原理,就要考虑下,这个东西能用来干啥。下面介绍下书上和网络上有关消息转发的应用场景:
1.JSPatch
JSPatch是一个热修复的第三方开源库。它的实现原理就是利用了消息转发机制。
具体来说,JSPatch是利用了第三步的NSInvocation对象,因为在消息转发的第一步和第二步,我们只能获取消息的选择子,而在第三步,我们可以通过NSInvocation获取当前消息的所有内容(接收者,选择子,参数值)。因此可以在第三步,获取参数值。
JSPatch具体是怎么做的呢?JSPatch 的基本原理就是:JS 传递字符串给 OC,OC 通过 Runtime 接口调用和替换 OC 方法。
2.实现属性的自动化存取
这里模仿实现一个《Effective Objective-C 2.0》书中描述的一个完整的例子:
下面示范如何用动态方法解析来实现@dynamic属性。实现一个“字典”对象,内部可以用字典存取其他对象,但是存取方式,要通过属性的set和get方式来实现。开发者只需要声明属性,并将属性声明为@dynamic。这样运行时系统就不会自动为属性生成相应的set和get方法,需要开发者自己去实现。如果属性比较少,我们可以手动书写相应的存取方法:
#import <Foundation/Foundation.h>
@class People;
@interface MFExampleDictionary : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) People *people;
@end
#import "MFExampleDictionary.h"
@interface MFExampleDictionary ()
@property (nonatomic, strong) NSMutableDictionary *storeDictionary;
@end
@implementation MFExampleDictionary
@dynamic name,people;
- (instancetype)init
{
self = [super init];
if (self) {
_storeDictionary = [NSMutableDictionary dictionary];
}
return self;
}
- (void)setName:(NSString *)name {
// 使用属性的set方法名为key -> setName:
NSString *key = NSStringFromSelector(_cmd);
NSLog(@"%@",key);
[_storeDictionary setObject:name forKey:key];
}
- (NSString *)name {
// 使用属性的set方法名为key -> setName:
NSString *get = NSStringFromSelector(_cmd);
NSString *key = getToSet(get);
NSLog(@"%@",key);
return [_storeDictionary objectForKey:key];
}
- (void)setPeople:(People *)people {
NSString *key = NSStringFromSelector(_cmd);
[_storeDictionary setObject:people forKey:key];
}
- (People *)people {
NSString *get = NSStringFromSelector(_cmd);
NSString *key = getToSet(get);
NSLog(@"%@",key);
return [_storeDictionary objectForKey:key];
}
NSString *getToSet(NSString *get) {
NSString *firstChar = [get substringToIndex:1];
NSString *upString = [get stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:[firstChar uppercaseString]];
NSString *setString = [NSString stringWithFormat:@"set%@:",upString];
return setString;
}
@end
如果需要存取的属性多达几百个呢?我们就需要编写大量的存取的方法。这时候自动转发机制就可以为我们所用了。直接看代码(头文件代码不变):
#import "MFExampleDictionary.h"
#import <objc/message.h>
@interface MFExampleDictionary ()
@property (nonatomic, strong) NSMutableDictionary *storeDictionary;
@end
@implementation MFExampleDictionary
@dynamic name,people;
- (instancetype)init
{
self = [super init];
if (self) {
_storeDictionary = [NSMutableDictionary dictionary];
}
return self;
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSString *selString = NSStringFromSelector(sel);
if ([selString hasPrefix:@"set"]) {
class_addMethod([self class], sel, (IMP)setMethod, "v@:@");
}else{
class_addMethod([self class], sel, (IMP)getMethod, "v@:");
}
return [super resolveInstanceMethod:sel];
}
void setMethod(MFExampleDictionary *self,SEL _cmd,id value) {
NSString *key = NSStringFromSelector(_cmd);
NSLog(@"%@",key);
[self.storeDictionary setObject:value forKey:key];
}
id getMethod(MFExampleDictionary *self,SEL _cmd) {
NSString *get = NSStringFromSelector(_cmd);
NSString *key = getToSet(get);
NSLog(@"%@",key);
return [self.storeDictionary objectForKey:key];
}
NSString *getToSet(NSString *get) {
NSString *firstChar = [get substringToIndex:1];
NSString *upString = [get stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:[firstChar uppercaseString]];
NSString *setString = [NSString stringWithFormat:@"set%@:",upString];
return setString;
}
@end
我们可以看到,利用消息转发,完成了这种设计,并且减少了代码量。
在iOS的CoreAnimation框架中CALayer类就用了与本例相似的实现方式,这使得CALyer成为“兼容于键值编码的”容器类,也就是说,能够向里面随意添加属性,然后以键值对的形式来访问。于是开发者就可以向其中新增自定义的属性了,这些属性的存储工作由基类直接负责,开发者只需要在CALyer的子类中定义新属性即可。
tips:这个用法主要参考书中描述的用法,其实我个人有点迷惑,既然是存储是数据,为什么一定要在对象内部放一个字典的方式来解决呢?直接用属性对应的实例变量来存储岂不是更好?如果需要以字典的形式输出,完全可以用模型转字典的方式来代替完成。所以对应这种用法的必要性有点质疑,如果有哪位同学有不一样的想法,欢迎留言指点!
3.模拟多重继承
模拟多重继承,其实就是利用第二步和第三步来实现的。此种应用场景也不常见,花里胡哨,个人感觉有点鸡肋(=、=)。
小结:
上述的原理,我们能这么干,说白了,还是苹果爸爸暴露出来的API,苹果爸爸给我们什么,我们用什么。值得思考的一点是,消息转发机制有什么作用呢?Apple为什么要这么设计呢?防止收到未知消息而崩溃吗?若是为了防止崩溃,必须提前知道哪些方法没有被实现,那么既然已经知道了,程序员在编程的时候在相应的类添加一下方法实现不就行了吗?为什么还要多此一举呢?
关于消息转发的应用场景目前就介绍这么多,我看网上有博客说还可以用来实现“多重代理”,这个有待考证。
如果哪位朋友关于消息转发有更深的认识,欢迎指教!
网友评论