前言:
之前一直有了解iOS中的消息转发机制,一直没有总结过。这篇文章就当是对之前碎片知识的总结吧。主要涉及到了runtime的消息传递和如果没有找到对象的方法,系统为我们提供的三次拯救机会。废话不多说,直接进行正文。
Runtime的消息传递
我们都知道OC是一门动态语言。OC语言并不能直接被计算机所识别。需要先转换成C语言,然后转成汇编语言,最后转成计算机认识的机器语言。
当我们调用一个对象的方法[objc foo]时,编译器会将代码进行转换成消息发送objc_msgSend(objc foo)。具体执行过程是这样的:
- 首先,系统通过objc的isa指针找到它对应的class。
- 然后,在class的method list的列表中查找方法foo。
- 如果没有找到对应的方法,就去它的父类中查找。
- 在这个过程中找到对应的方法,就去通过方法的IMP指针(方法地址指针)执行方法。
当然有的时候我们并没有实现方法foo。例如我们调用了一个不可变字符串的修改方法。其实,修改方法只实现在可变字符串类中。那么程序就会崩溃,报错信息unrecognized selector。不能识别到这个方法。其实在这个崩溃之前,系统给我们提供了三次挽救的机会,这就是消息转发过程。
消息转发
当系统并不能找到你要执行的方法时,会提供三次机会拯救你的程序崩溃。
1、动态方法解析
oc在运行时,对象方法调用会执行 +resolveInstanceMethod: 类方法调用会执行 +resolveClassMethod:方法,这样我们可以在这两个方法中,进行一个处理。通过runtime的动态添加方法,并且将消息转发给备用的接收方法。
可以看到,我并没有实现foo:方法,但是运行程序并没有崩溃,而且执行了我新定义的newFooMethod方法。
然后,如果这一步我们没有进行相应的处理,程序就会进行到第二步,寻找消息的实现对象。
2、定义备用接收者
如果第一步,没有进行拯救,那么程序会进行到这一步,系统会调用- (id)forwardingTargetForSelector:(SEL)aSelector方法。给你一个将消息传递给其他对象的机会。因此,如果想在这一步进行拯救。我们需要定义一个备用的类,来作为处理消息的target。只要我们备用的类中实现了这个方法,那么系统将执行这个方法。
#import <objc/runtime.h>
@interface OtherPerson : NSObject
@end
@implementation OtherPerson
//定义foo方法,处理转发进来的foo方法
- (void)foo {
NSLog(@"Person foo func");
}
@end
@implementation TestViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self performSelector:@selector(foo)];
// Do any additional setup after loading the view.
}
void newFooMethod(){
NSLog(@"Now I am new foo func.");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel{
// if(sel == @selector(foo)){
// class_addMethod([self class], sel, (IMP)newFooMethod, "v@:");
// return YES;
// }
//直接执行系统方法,不走上边的添加方法,那么找不到就会走下边的转发
return [super resolveInstanceMethod:sel];
}
- (id)forwardingTargetForSelector:(SEL)aSelector{
if(aSelector == @selector(foo)){
return [OtherPerson new];//设置转发的target处理方法
}
return [super forwardingTargetForSelector:aSelector];
}
/*
#pragma mark - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
// Get the new view controller using [segue destinationViewController].
// Pass the selected object to the new view controller.
}
*/
@end
然后运行代码,发现打印了Person foo func。也没有崩溃,说明foo方法已经通过转发给OtherPerson类来执行了。
3、完整消息转发
如果前两个方法没有进行拯救成功,那么消息转发进入到最后一步。如果这一步还不能处理消息,那么Runtime则会发出-doesNotRecognizeSelector:消息,程序这时也就挂掉了。
在这里我们需要更大的代价来进行拯救,我们需要先获得方法签名。将方法签名传递给系统的
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector方法。当这个方法接收到方法签名后,
runtime就会创建一个NSInvocation对象,发送- (void)forwardInvocation:(NSInvocation *)anInvocation给目标对象
。
#import <objc/runtime.h>
@interface OtherPerson : NSObject
@end
@implementation OtherPerson
- (void)foo {
NSLog(@"Person foo func");
}
@end
@interface TestViewController ()
@end
@implementation TestViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor yellowColor];
[self performSelector:@selector(foo)];
// Do any additional setup after loading the view.
}
void newFooMethod(){
NSLog(@"Now I am new foo func.");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel{
// if(sel == @selector(foo)){
class_addMethod([self class], sel, (IMP)newFooMethod, "v@:");
// return NO;
// }
//走系统的方法,找不到方法,走第二步
return [super resolveInstanceMethod:sel];
}
- (id)forwardingTargetForSelector:(SEL)aSelector{
// if(aSelector == @selector(foo)){
// return [OtherPerson new];
// }
//执行系统的方法,因为没有响应的类,所以会进入到第三步,也就是下边的方法
return [super forwardingTargetForSelector:aSelector];
}
//这是拯救崩溃的最后一步
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if([NSStringFromSelector(aSelector) isEqualToString:@"foo"]){
//方法选择器
SEL fooSEL = @selector(foo);
//方法签名
NSMethodSignature *fooSignature = [NSString methodSignatureForSelector:fooSEL];
//返回一个方法签名
return fooSignature;
}
return [super methodSignatureForSelector:aSelector];
}
//通过方法签名进行转发
- (void)forwardInvocation:(NSInvocation *)anInvocation{
SEL sel = anInvocation.selector;
OtherPerson *person = [OtherPerson new];
if([person respondsToSelector:sel]){
[anInvocation invokeWithTarget:person];
}else{
//没有办法响应
[self doesNotRecognizeSelector:sel];
}
}
/*
#pragma mark - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
// Get the new view controller using [segue destinationViewController].
// Pass the selected object to the new view controller.
}
*/
@end
通过运行程序,打印了Person foo func,没有出现程序崩溃。至此,消息转发三次拯救机会就完事了。
下边稍微说一下方法签名
方法签名的获取方式有两种,一种是手动生成方法签名,另一种是自动生成方法签名。
手动生成方法签名
手动生成方法签名,需要传递方法的返回值,参数等信息来通过方法生成签名。例如[NSMethodSignature signatureWithObjCTypes:"@@*"]
这里边参数是一个字符串数组。这个数组包含了方法的返回值、参数等信息。其中第一个@表示方法返回值是一个id类型的对象,而第二个@表示方法接收一个id参数,最后的*表示方法接收一个字符串(char*)类型的参数。其实苹果官方有一个对照表TypeEncoding
自动生成方法签名
一般情况下,我们是通过自动生成方法签名的。也就是我上边代码的形式,不用传递一个c的字符串数组表示方法的返回值参数信息。生成起来也更准确一些,有下边两个方法,都可以生成方法的签名。
// 从类中获取实例方法签名
fooSignature = [NSString instanceMethodSignatureForSelector:fooSEL];
// 从类中获取类方法签名
fooSignature = [NSString methodSignatureForSelector:fooSEL];