iOS消息转发机制及避免崩溃的解决方案

1,511 阅读6分钟

最近重温了一下iOS的消息转发机制,让自己理解的更深刻。 消息的转发分为三步:

  1. 方法解析
  2. 快速转发
  3. 慢速转发

消息转发的整个流程: 我们知道调用对象的某一个方法的时候其实就是再给这个对象发消息,来调用他的方法 Person,我们调用他的 sendMessage: 方法,调用如下:

[[Person new] sendMessage:@"Hello"];

这个时候消息的转发流程如下:

第一次转发:方法解析 Person 尝试自己解析这个方法,此时 Person 类的 +(BOOL)resolveClassMethod:(SEL)sel (类方法) 或者 +(BOOL)resolveInstanceMethod:(SEL)sel (实例方法) 会执行,如果 Person 类实现了 sendMessage: 这个方法,那么会被调用,消息转发也就结束。

第二次转发:快速转发 如果第一次转发方法的实现没有被找到,那么会调用这个类的 -(id)forwardingTargetForSelector:(SEL)aSelector 开始进行消息快速转发,如果我们可以找到一个对象来实现调用的sendMessage: 方法,可以在这个方法里将这个对象返回,然后该对象就会执行我们调用的 sendMessage: 方法

第三次转发:慢速转发 如果第二次转发也没有找到可以处理 sendMessage: 方法的对象的话,那么 Person 类的 -(NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector 方法就会被调用,这个方法会返回一个 sendMessage: 的方法签名,如果返回了 sendMessage: 的方法签名的话,Person 类的 -(void)forwardInvocation:(NSInvocation*)anInvocation 方法会被调用,在这个方法里处理我们调用的方法,如果这个方法里也处理不了的话,就会执行 doesNotRecognizeSelector 方法,引起一个 unrecognized selector sent to instance 异常崩溃。

void sendMessage(id self, SEL _cmd, NSString * msg) {
    NSLog(@"----%@", msg);
}

@implementation Person
// 正常的消息转发
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSString * methodName = NSStringFromSelector(sel);
    if ([methodName isEqualToString:@"sendMessage:"]) {
        return class_addMethod(self, sel, (IMP)sendMessage, "v@:@");
    }
    return NO;
}
// 快速转发
- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSString * methodName = NSStringFromSelector(aSelector);
    if ([methodName isEqualToString:@"sendMessage:"]) {
        return [Man new];
    }
    return [super forwardingTargetForSelector:aSelector];
}
// 1. 方法签名
// 2. 消息转发
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSString * methodName = NSStringFromSelector(aSelector);
    if ([methodName isEqualToString:@"sendMessage:"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    }
    return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = [anInvocation selector];
    Man * tempObj = [Man new];
    if ([tempObj respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:tempObj];
    }else {
        [super forwardInvocation:anInvocation];
    }
    
}
- (void)doesNotRecognizeSelector:(SEL)aSelector {
    NSLog(@"消息转发失败");
}

@end


我们如何利用IOS的消息转发机制来避免对象没有实现某个方法的时候出现崩溃的情况呢?我们来看一下如果在每一步里进行避免:

在第一次转发里拦截崩溃 — 消息处理

假如 Person 类没有实现 sendMessage: 方法,那么我们需要在第一次消息转发调用 +(BOOL)resolveClassMethod:(SEL)sel (类方法) 或者 +(BOOL)resolveInstanceMethod:(SEL)sel (实例方法) 这两个方法的时候在里面加上方法的实现,代码如下 (以+(BOOL)resolveInstanceMethod:(SEL)sel 为例 ):

#pragma mark - 第一步 方法解析
void sendMessage(id self, SEL _cmd, NSString * msg) {
    NSLog(@"--%@", msg);
}

+(BOOL)resolveInstanceMethod:(SEL)sel{
    if ([super resolveInstanceMethod:sel]) {
        return YES;
    }else{
        class_addMethod(self, sel, (IMP)sendMessage, "v@:@");
        return YES;
    }
}

我们需要自己实现一个 sendMessage: 方法,然后在resolveInstanceMethod方法里首先判断自己的父类是否可以解析该方法,如果父类解析不了的话我们就利用iOS的runtime机制动态的将 sendMessage 添加到自己的类里,前提是我们自己有一个 sendMessage 的方法实现(c语音),程序运行后正常输出: --Hello

这就是通过消息转发机制的第一步避免崩溃的操作

在第二次转发里拦截崩溃 — 快速转发

快速转发阶段调用Dog类的 -(id)forwardingTargetForSelector:(SEL)aSelector 方法,如果我们自己实现不了 sendMessage: 方法的话,我们需要创建一个实现了这个方法的对象然后抛出去来作为方法的处理对象, 假如我们有一个 Man 类,并且这个 Man 实现了 sendMessage 方法,

只要这个方法返回的不是nil和self,整个消息发送的过程就会被重启

此时的处理方法如下:

// 快速转发
- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSString * methodName = NSStringFromSelector(aSelector);
    if ([methodName isEqualToString:@"sendMessage:"]) {
        return [Man new];
    }
    return [super forwardingTargetForSelector:aSelector];
}

我们只需要创建一个 Man 的实例抛出去就可以了,该实例会处理 sendMessage: 方法。 运行后,Man 里的方法会执行,程序正常运行。

这就是通过消息转发机制的第二步避免崩溃的操作。

在第三次转发里拦截崩溃 — 快速转发

此时进入第三次转发阶段了,在这里会调用 -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

消息获得函数的参数和返回值, 如果返回nil, runtime则会发出doesNotRecognizeSelector消息, 然后crash, 若是返回了一个函数签名, 就会创建一个NSInvocation对象并发送 -(void)forwardInvocation:(NSInvocation *)anInvocation 方法寻找处理方法签名的对象, 我们先看一个比较普通的处理措施,其实跟第二步一样,也是返回一个可以处理 sendMessage: 方法的对象:

// 1. 方法签名
// 2. 消息转发
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSString * methodName = NSStringFromSelector(aSelector);
    if ([methodName isEqualToString:@"sendMessage:"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    }
    return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = [anInvocation selector];
    Man * tempObj = [Man new];
    if ([tempObj respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:tempObj];
    }else {
        [super forwardInvocation:anInvocation];
    }
    
}

运行后,Man 里的方法会执行,程序正常运行。 这就是通过消息转发机制的第三步避免崩溃的操作。

如果都失败的话, 则会调用 - (void)doesNotRecognizeSelector:(SEL)aSelector
方法, 给系统报错, 该怎么修改就怎么修改的吧

- (void)doesNotRecognizeSelector:(SEL)aSelector {
    NSLog(@"asdsa");
}

扩展: 我们可以看到在第三次转发的时候我们可以拿到 anInvocation 参数,这个参数里其实包含了方法调用的所有信息,我们可以对这个 anInvocation 进行一些修改,增加或者减少参数,然后找到适当的对象来进行处理,或者我们自己实现方法自己处理。 举例如下:

// 1. 方法签名
// 2. 消息转发
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSString * methodName = NSStringFromSelector(aSelector);
    if ([methodName isEqualToString:@"sendMessage:"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    }
    return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    //重新指定的方法
    SEL sel = @selector(sendMessage2:);
    // 给方法签名
    NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    anInvocation = [NSInvocation invocationWithMethodSignature:signature];
    // 添加target
    [anInvocation setTarget:self];
    [anInvocation setSelector:sel];
    // 添加参数
    NSString *message = @"修改了的参数";
    [anInvocation setArgument:&message atIndex:2];
    if ([self respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:self];
    }else{
        [super forwardInvocation:anInvocation];
    }    
}
- (void)sendMessage2:(NSString *)msg {
    NSLog(@"--%@", msg);
    // --修改了的参数
}

本来我们调用的方法只是一个 sendMessage:,不包含任何参数的,但是通过在 -(void)forwardInvocation:(NSInvocation *)anInvocation 方法里我们对 anInvocation 进行了改动,使其变成了一个对带有一个 NSString 类型参数的调用,然后我们实现了这个方法,并且把自己设置为方法的实现对象,程序运行后输出: --修改了的参数

这样就进行了简单的方法和参数替换的操作, 代码不复杂, 可以自己试试

PS: 消息转发机制的其他用途: 1.实现多重代理 2.间接实现多继承


End