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

350 阅读6分钟

最近研究了一下iOS的消息转发机制,特此做记录,用一个真实的例子让自己理解的更深刻。这个例子会列举方法没有实现而导致崩溃的避免措施,。息的转发分为三步,通过这个例子看一下在每一步的转发中如何避免因为方法没有实现而导致的程序崩溃。

先说一下消息转发的整个流程吧,我们知道调用对象的某一个方法的时候其实就是再给这个对象发消息,来调用他的方法,假如我们有一个Dog类,我们调用他的testFun方法,调用如下:

Dog *testDog = [Dog new];
 [testDog performSelector:@selector(testFun)];

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

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

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

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

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

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

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

#pragma mark - 第一步 方法解析
void testFun(){
    NSLog(@"Dog test Fun");
}

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

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

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

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

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

#pragma mark - 第二步快速转发
-(id)forwardingTargetForSelector:(SEL)aSelector{
    if ([NSStringFromSelector(aSelector) isEqual:@"testFun"]) {
        return [SubDog new];
    }
    return [super forwardingTargetForSelector:aSelector];
}

其实很简单,我们只需要创建一个SubDog的实例抛出去就可以了,该实例会处理testFun方法。 运行后,SubDog里的方法会执行,程序正常运行。 这就是通过消息转发机制的第二步避免崩溃的操作。

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

此时进入第三次转发阶段了,在这里会调用 -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector 返回放方法签名,如果返回了的话会调用 -(void)forwardInvocation:(NSInvocation *)anInvocation 方法寻找处理方法签名的对象,我们先看一个比较普通的处理措施,其实跟第二步一样,也是返回一个可以处理testFun方法的对象:

#pragma mark - 第三步 慢速转发
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if ([super methodSignatureForSelector:aSelector] == nil) {
	//创建一个方法签名然后返回
        NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
        return signature;
    }
    return [super methodSignatureForSelector:aSelector];
}

-(void)forwardInvocation:(NSInvocation *)anInvocation{
	//创建一个对象来处理testFun方法
    SubDog *subDog = [SubDog new];
    SEL sel = anInvocation.selector;
    if ([subDog respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:subDog];
    }else{
	//如果处理不了的话,调用doesNotRecognizeSelector方法返回崩溃
        [self doesNotRecognizeSelector:sel];
    }
}

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

在第三次转发的时候可以做的更多的东西

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

#pragma mark - 第三步 慢速转发
-(void)showMessage:(NSString*)message{
    NSLog(@"message = %@",message);
}

-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if ([super methodSignatureForSelector:aSelector] == nil) {
        NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
        return signature;
    }
    return [super methodSignatureForSelector:aSelector];
}

-(void)forwardInvocation:(NSInvocation *)anInvocation{
    SEL sel = @selector(showMessage:);
    NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    anInvocation = [NSInvocation invocationWithMethodSignature:signature];
    [anInvocation setTarget:self];
    [anInvocation setSelector:sel];
    NSString *message = @"在第三步自己实现的方法,改了参数";
    [anInvocation setArgument:&message atIndex:2];
    if ([self respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:self];
    }else{
        [super forwardInvocation:anInvocation];
    }
}

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

这样就实现了在第三次调用里进行了偷梁换柱的操作,代码很简单,但很有意思,自己操作试试吧。

消息转发机制的其他用途: 1.JSPatch --iOS动态化更新方案利用的就是消息转发机制的第三次转发 2.实现多重代理 3.间接实现多继承