iOS的消息转发机制详解

iOS开发过程中,有一类的错误会经常遇到,就是找不到所调用的方法,当然这类问题比较好解决,给当前对象或其父类对象添加该方法即可,使得编译器在编译时能正确找到该方法;或者,还有另外的方法,由于Objective-C是一门动态语言,我们也可以在运行期再给类添加该方法,一样可以解决该问题,而这就涉及了类的消息转发机制。

本文就主要来介绍一下iOS系统的消息转发机制,探究一下在调用一个方法时,如果本类中没有该方法时,对象究竟是如何进行消息转发的,来避免程序抛出异常。

异常现象

当调用的对象方法不存在,即使经过消息转发也不存在时,就会抛出下面的异常

 -[Teacher playPiano]: unrecognized selector sent to instance 0x6000000114c0
 *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Teacher playPiano]: unrecognized selector sent to instance 0x6000000114c0'

解决办法

针对上述的异常问题,最简单的方法就是直接在类中添加playPiano方法,或者在其继承树中添加该方法,均可以解决该问题,所以这种方法再次不再赘述,下面介绍一下如何利用消息转发机制解决该问题。

消息转发是在运行时进行的,大致分为两个阶段:第一阶段是先检查接收者,看是否能通过runtime动态添加一个方法,来处理这个unknown selector的消息;第二阶段就是完整的消息转发机制,首先会先查看有没有其它对象能够处理该消息,如果没有,就把该消息的全部信息封装到NSInvocation对象中,看那个对象能否处理,如果还无法处理,就查看继承树中的类是否能够处理该消息,如果到NSObject之前都无法处理该消息,那么最后就会调用NSObject类的doesNotRecognizeSelector方法来抛出异常,表明调用的方法不存在。

1.动态方法解析

对象在收到无法处理的消息时,会调用下面的方法,前者是调用类方法时会调用,后者是调用对象方法时会调用

// 类方法专用
+ (BOOL)resolveClassMethod:(SEL)sel
// 对象方法专用
+ (BOOL)resolveInstanceMethod:(SEL)sel

在该方法中,需要给对象所属类动态的添加一个方法,并返回YES,表明可以处理

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    NSString *method = NSStringFromSelector(sel);
    if ([@"playPiano" isEqualToString:method]) {
        /**
         添加方法
         
         @param self 调用该方法的对象
         @param sel 选择子
         @param IMP 新添加的方法,是c语言实现的
         @param 新添加的方法的类型,包含函数的返回值以及参数内容类型,eg:void xxx(NSString *name, int size),类型为:v@i
         */
        class_addMethod(self, sel, (IMP)playPiano, "v");
        return YES;
    }
    return NO;
}

2.备援接受者

经历了第一步后,如果该消息还是无法处理,那么就会调用下面的方法,查询是否有其它对象能够处理该消息

- (id)forwardingTargetForSelector:(SEL)aSelector

在这个方法里,我们需要返回一个能够处理该消息的对象

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    NSString *seletorString = NSStringFromSelector(aSelector);
    if ([@"playPiano" isEqualToString:seletorString]) {
        Student *s = [[Student alloc] init];
        return s;
    }
    // 继续转发
    return [super forwardingTargetForSelector:aSelector];
}

3.完整的消息转发

经历了前两步,还是无法处理消息,那么就会做最后的尝试,先调用methodSignatureForSelector:获取方法签名,然后再调用forwardInvocation:进行处理,这一步的处理可以直接转发给其它对象,即和第二步的效果等效,但是很少有人这么干,因为消息处理越靠后,就表示处理消息的成本越大,性能的开销就越大。所以,在这种方式下,会改变消息内容,比如增加参数,改变选择子等等。

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
- (void)forwardInvocation:(NSInvocation *)anInvocation

下面是改变选择子的例子,比如我们直接调用的是playPiano方法,最后转发给了traval:方法,完整实例参考:MsgSendDemo

// 完整的消息转发
- (void)travel:(NSString*)city
{
    NSLog(@"Teacher travel:%@", city);
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    NSString *method = NSStringFromSelector(aSelector);
    if ([@"playPiano" isEqualToString:method]) {
        
        NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:@"];
        return signature;
    }
    return nil;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    SEL sel = @selector(travel:);
    NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    anInvocation = [NSInvocation invocationWithMethodSignature:signature];
    [anInvocation setTarget:self];
    [anInvocation setSelector:@selector(travel:)];
    NSString *city = @"北京";
    // 消息的第一个参数是self,第二个参数是选择子,所以"北京"是第三个参数
    [anInvocation setArgument:&city atIndex:2];
    
    if ([self respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:self];
        return;
    } else {
        Student *s = [[Student alloc] init];
        if ([s respondsToSelector:sel]) {
            [anInvocation invokeWithTarget:s];
            return;
        }
    }
    
    // 从继承树中查找
    [super forwardInvocation:anInvocation];
}

iOS的消息转发机制给我们提供了更多的选择,来保证消息的正常传递,而了解这些具体的实现方法,则可以让我们的程序更加的健壮。

参考资料

iOS消息转发机制详解

iOS 消息转发机制(VN的逃生之路)

NSMethodSignature和NSInvocation的用法

使用NSMethodSignature和NSInvocation实现消息转发

posted @ 2017-07-21 09:58  飞鱼湾  阅读(6919)  评论(0编辑  收藏  举报