前言
今天我们再来通过另外一个机制来感受一下OC的动态特性吧,那就是OC的消息转发机制
在之前的不一样的OC中我们有提到,OC是消息型语言,OC中的方法调用其实只是传递消息而已,编译器并不能决定程序真正执行的到底是哪段代码,这个工作,需要运行时系统来完成.
那我们今天要讨论的消息转发又是什么呢?
既然OC的方法调用实际上都是消息传递,那传递的消息内容和时机其实编译器并不能完全控制,这时我们是不是能够联想到这样的场景:一个对象接受了一个编译器控制外的消息,这个对象对这个消息无从下手,他并不具备解决这个消息的能力(没有实现相应方法),这个时候该怎么办呢?
当遇到这种情况的时候,我们的消息转发机制就被触发了,系统将利用这套机制竭力帮我们控制这个糟糕的局面,不至于让程序崩溃,但是如果消息转发机制竭尽全力通过各种办法也无法解决这个意料之外的消息时,那么程序就会崩溃,并产生那个经典的崩溃日志:
unrecognized selector sent to instance 0x60000000b480
消息转发机制的三部曲
消息转发机制在极力为我们收拾残局的时候,会有三步措施,如果在最坏的情况下,这三个步骤将会按顺序依次执行,如果这三个步骤都无法挽回,那么程序将会崩溃,值得一提的是:如果消息转发机制的某一个步骤就能解决这个问题,那么下一个步骤就不会执行了,消息转发机制就到此为止了(事情都解决了,当然不需要下一步了)
我们现在用一个简单的例子来触发消息转发机制,并利用消息转发机制解决问题
我新建了一个叫做Person的类,并在它的.h文件里为Person声明了一个实例方法:run
以下是.h文件的内容.
#import <Foundation/Foundation.h>
@interface Person : NSObject
-(void)run;
@end
大家猜猜我在.m文件里做了什么?以下是.m文件的内容:
#import "Person.h"
@implementation Person
@end
哈哈,我什么都没做,甚至没有对.h中的run方法写实现.
我为什么要这么做呢?这样我们就可以欺骗编译器,在调用这个方法的时候,编译器默认我们已经对这个方法写了实现,他就会让我们舒舒服服的调用这个方法,并不会报错,组织我们队这个方法的调用
那接下来,我们在控制器中新建一个person对象,并调用这个方法的run方法
Person *p1=[Person new];
[p1 run];
大家应该猜的到会发生什么了吧,我们能够成功编译并运行这个程序,但是当代码执行到了run方法时,程序崩溃了,并报了我们常见的那个错误unrecognized selector sent to instance 0x60000000b480
大家一定想知道这其中到底发生了什么事情:当run方法被调用时,运行时系统就会对p1这个对象发送一条run的消息,意料之中的是p1这个对象并不能对相应这个消息,因为我们没有写run的实现方法,那接下来就会触发消息转发机制了,那既然触发了消息转发机制,为什么还是崩溃了呢,因为,这就是那种最坏的情况,消息转发机制三个步骤依次执行却依然没有挽回崩溃的结果.
那么我们快看一看怎么让消息转发机制发挥作用吧.
步骤一:能不能动态添加一个函数解决这个消息啊
在这一步中,面对无法解决的消息,消息转发机制首先做的是,看看能否动态添加一个方法实现,来解决这个消息.
如果是实例方法,系统会调用这个这个实例所属类的类方法:
+(BOOL)resolveInstanceMethod:(SEL)sel
如果是类方法:
+(BOOL)resolveClassMethod:(SEL)sel
这对方法如何使用呢?我们来看:
我在person类的.m文件中写了如下代码:
+(BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel==@selector(run)) {
class_addMethod(self, sel, (IMP)run , "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
void run (id self,SEL _cmd)
{
NSLog(@"fdfsfaf");
}
我们写了一个run的c语言函数,当消息机制触发时,selector为run时那么就会利用运行时库动态添加一个方法,而这个方法的函数实现是我们用C写的一个函数,这个函数的两个参数为self,和_cmd,因为OC方法的本质就是至少包含两个参数的C函数这两个参数便是隐藏的self,和_cmd,前者是消息接受者,后者是一个SEL指针
通过这样的操作,系统就为我们通过动态添加一个方法的方式来解决了这个不能响应的消息
这也是消息转发机制的第一步,如果这一步并没有解决这个问题,那便要有下面的步骤了.
步骤二:能不能找别人处理这个消息啊
当上一步没有解决这个问题的时候,这一步,系统将会通过寻找有没有别的对象可以帮忙处理这条消息.
系统会调用如下方法:
-(id)forwardingTargetForSelector:(SEL)aSelector
{
return [RunPerson new];
}
大家可以看到,我在这个方法中返回了一个新的类RunPerson的实例,而在这个类的.m文件中,我写了如下实现:
-(void)run
{
NSLog(@"我是会跑的人,我能跑!");
}
通过返回一个新的类,我们把这个消息转发给了一个其它对象,让这个对象来代替我们解决这个消息.
步骤三: 完整的消息转发
在这一步中,消息转发机制将进行最后一步操作
我在Person的.m文件中写了如下代码,将会使得run消息,在消息转发机制最后一步得到处理
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
if (aSelector==@selector(run)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector: aSelector];
}
-(void)forwardInvocation:(NSInvocation *)anInvocation
{
SEL selector =[anInvocation selector];
RunPerson *RP1=[RunPerson new];
RunPerson *RP2=[RunPerson new];
if ([RP1 respondsToSelector:selector]) {
[anInvocation invokeWithTarget:RP1];
}
if ([RP2 respondsToSelector:selector]) {
[anInvocation invokeWithTarget:RP2];
}
}
第一个方法返回的是一个方法签名,而代码中的v@:表示这个函数的性质,v代表返回值为void,@代表self,:代表_cmd;
当有了方法签名,消息转发机制将会调用-(void)forwardInvocation:(NSInvocation )anInvocation方法,这个方法的参数anInvocation*为这个消息的全部内容,包括调用者,参数等等.
在这个方法里我们初始化了两个RunPerson实例,并把这个Invocation转发给了这两个RunPerson实例,这样,这两个RunPerson对象就都会对这个消息进行处理了
这也是与第二个步骤最大的区别,可以转发给多个对象进行处理.
到此,消息转发的三个步骤就结束了,如果到第三步还没有解决这个消息,那么程序就会崩溃了.