iOS - 消息转发机制
我们知道,OC是动态语言,所有的方法都会以消息的形式传递给对象,对象会根据方法的类型来进行实例方法或者类方法的选择,当实例方法和类方法都不可以响应这个消息时,程序并不会立即报错,而是启动消息转发机制来处理这个消息,当消息转发机制都无法处理这个消息时,程序才会crash,并提示“unrecognized selector sent to instance ****” 本篇文章主要是详细记录当对象找不到对应的方法时,消息转发的具体机制
具体分为3部分:
屏幕快照 2017-08-16 14.19.41.png
1、动态方法解析
+ (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
+ (BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
这两个方法是消息转发机制的第一步,分别对应对象的类方法和实例方法,当对象接收到不可处理的消息时,会首先查阅此对象是否实现了对应的对象方法或者类方法,在这个方法内,我们可以通过runtime动态的添加对象的实例方法,从而使这个对象拥有响应这个消息的能力
// Person.h
@interface Person : NSObject
- (void) eatFood:(NSString *)food;
@end
// Person.m
- (void)personEatFood
{
NSLog(@"person eat food");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if(sel == @selector(eatFood:)) {
Method method = class_getInstanceMethod([self class], @selector(personEatFood));
class_addMethod([self class], sel, method_getImplementation(method), "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
// ViewController.m
id p = (id)[[Person alloc] init];
[p eatFood:@"VCfood"];
上述代码可以看出,在resolveInstanceMethod:方法中,我们动态的为Person类添加了eatFood:方法,所以程序在运行时没有crash,而是正常的打印“person eat food” .
屏幕快照 2017-08-16 11.09.47.png
简单说一下class_addMethod方法的含义:
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
从名字可以很容易的看出,是为某个类添加方法的runtime函数,它有四个参数:
Class cls: 为哪个类添加方法
SEL name: 添加方法的名字
IMP imp:添加方法的内容(这里是C函数,可以用method_getImplementation直接获取OC方法对应的C函数)
const char *type: type encoding 用来描述函数的参数和返回值 "v@:" v代表返回值为void ,@表示self,:表示selector,具体可参考官方介绍
2、备援接收者
如果对象没有在第一步(动态消息转发)中没有动态的添加可以响应消息的方法,那runtime就会走到这一步,这一步,我们可以修改处理消息的对象,也就是,将这个消息转给其他的对象处理
- (id)forwardingTargetForSelector:(SEL)aSelector OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
这个方法返回一个对象(这个对象被称为备援对象),然后runtime会将消息发给这个对象让其处理,这一步需要注意的是,我们只可以改变接收消息的对象,但是无法改变消息本身(包括消息的名字以及参数等)
// 接着刚才的栗子,我们将resolveInstanceMethod:删除,添加如下方法
//Person2.h
@interface Person2 : NSObject
- (void) eatFood:(NSString *) food;
@end
//Person.m
@implementation Person2
- (void)eatFood:(NSString *)food
{
NSLog(@"person2 eat food ---- %@",food);
}
@end
//Person.m
- (instancetype) forwardingTargetForSelector:(SEL)aSelector
{
if(aSelector == @selector(eatFood:)) {
return [[Person2 alloc] init];
}
return [super forwardingTargetForSelector:aSelector];
}
// ViewController.m
id p = (id)[[Person alloc] init];
[p eatFood:@"VCfood"];
我们可以看到,Peson2这个对象里实现了和Person一样的方法eatFood: 我们在forwardingTargetForSelector:方法里将消息的处理者从Person转为了Person2,所以程序没有crash,并输出了正常打印
屏幕快照 2017-08-16 11.44.37.png
3、完整的消息转发
所谓完整的消息转发在于,系统会创建NSInvocation对象,里面包含消息的所有内容(包括消息的名字,参数等),在这一过程,我们可以修改消息的内容,并将它发送给目标对象
//删除掉Person.m的forwardingTargetForSelector: 方法 并添加如下方法
//Person.m
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
return [NSMethodSignature signatureWithObjCTypes:"v@:@@"]; //返回方法签名,与最新的选择子一致,本例中添加了一个参数,所以签名编程"v@:@@" :后的两个@代表两个参数
}
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
[anInvocation setSelector:@selector(personEat1:Eat2:)];
NSString *eat2 = @"food2";
[anInvocation setArgument:&eat2 atIndex:3];
[anInvocation invokeWithTarget:self];
}
- (void) personEat1:(NSString *)eat1 Eat2:(NSString *)eat2
{
NSLog(@"person eat food1=%@ , food2=%@",eat1,eat2);
}
可以看到,我们将选择子(eatFood:)进行了更新(personEat1:Eat2)同时添加了第二个参数(@"food2"),然后将消息又交给了self,这时,因为self实现了personEat1:Eat2: 方法,所以程序没有crash,且有正常输出
屏幕快照 2017-08-16 14.27.26.png
以上就是消息转发的三个步骤,我们可以在实际的开发中合理的运用这些方法来动态的改变消息的具体表现形式~