iOS中的崩溃拦截 - 消息转发

引言

在iOS的开发中,线上崩溃是一件令人十分头疼的事情,每个需求的上线初期都让团队的人忐忑不安,由于崩溃率指标问题,我们团队专门做了这么一期崩溃拦截系统的需求来降低崩溃率。其中最值得分享的一则崩溃应该是未找到方法(unrecognized selector),这个崩溃在OC的项目中很常见,有一些就真的只是未实现该方法,有一些则是做类型转换时对应错误到值没有找到对应方法,还有一些比较隐晦的可能是在方法交换时交换了对方没有的方法,不管什么原因,如果没有适当的处理都会导致出现unrecognized selector的崩溃。

那么该怎么处理拦截这类崩溃呢?就是消息转发机制。

消息转发机制

在该类崩溃出现之前其实我们有三个机会可以来处理它

1.动态方法解析(Dynamic Method Resolution)

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    
}

+ (BOOL)resolveClassMethod:(SEL)sel{
    
}

在这一阶段我们可以尝试在运行时为对象添加为实现的方法,并返回YES表示我们已经处理了该异常,如果我们什么都不做,该方法放回NO,就会进入消息转发阶段。

2.消息快速转发(Fast Forwarding)

- (id)forwardingTargetForSelector:(SEL)aSelector{
    
}

在快速转发这一步中,系统首先会调用forwardingTargetForSelector:方法询问当前对象是否可以将消息转发给另一个对象,如果我们在这个方法中返回一个对象,那么就结束了消息转发流程,转到另一个对象来处理处理消息。但是如果我们返回nil,或者未处理该方法,那么消息将会进入慢速转发流程。

3.慢速转发(Normal Forwarding)

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    
}

快速转发流程失败后,系统就会进入慢速转发阶段,首先会调用methodSignatureForSelecto:方法来获取方法签名,如果返回`nil`,消息传递过程就会停止,并抛出`unrecognized selector`异常。如果提供了方法签名,系统接着会调用`forwardInvocation:`方法,你可以在这里进一步处理未识别的消息,例如记录日志或执行备用行为。

实现拦截

虽然每个阶段我们都可以进行拦截和处理,但是通常来讲我们会在消息转发的阶段进行拦截,因为消息转发机制提供了更灵活,面向对象的方法来处理未实现的方法调用,而且更容易维护,更灵活,代码可读性高。还避免了污染类的方法列表。

我们可以通过重写NSObject的forwardingTargetForSelector:方法来实现这个功能,也可以进行方法交换,这这里我们以对象方法为例,使用方法交换的方式来实现这个功能。使用方法交换而不是重写的好处是更灵活,对于不需要我们处理的方法,我们可以直接让它去执行原来的方法。

1.交换方法

创建用作方法交换的方法,这里面有个注意的一点在方法交换时为没有的方法动态添加了方法,避免交换父类的方法导致坑爹的现象,虽然在我们这个场景中不会出现,但在进行方法交换时这是一个很容易被忽视的问题。

//交换实例方法
bool cmRuntimeSwizzleMethod(Class class1,SEL selector1,Class class2, SEL selector2) {
    
    Method method1 = class_getInstanceMethod(class1, selector1);
    Method method2 = class_getInstanceMethod(class2, selector2);
    
    if (!method1 || !method2) {
        return  false;
    }
    //为class添加方法,否则可能会交换父类的方法(添加失败 就说明class已经有这个方法)
    class_addMethod(class1, selector1, method_getImplementation(method1), method_getTypeEncoding(method1));
    class_addMethod(class2, selector2, method_getImplementation(method2), method_getTypeEncoding(method2));
    //执行交换
    method_exchangeImplementations(class_getInstanceMethod(class1, selector1), class_getInstanceMethod(class2, selector2));
    return  true;
}

+ (BOOL)cmRuntimeSwizzleSelector:(SEL)selector1 withSelector:(SEL)selector2{
    return cmRuntimeSwizzleMethod(self, selector1, self, selector2);
}

2.实现cmForwardingTargetForSelector:方法

实现cmForwardingTargetForSelector:进行消息转发,转发的指定的类中。

- (id)cmForwardingTargetForSelector:(SEL)selector{
    
    //判断是否需要拦截,过滤系统方法
    if ([self isEqual:[NSNull null]] || ![self cmOverrideForwardingMethods]) {
        return [[LMCrashDetector alloc] initWithInvoker:self.class selector:selector];
    }
    
    return [self cmForwardingTargetForSelector:selector];
}

3.判断是否需要转发

在上面的转发代码之前我们进行了一个特殊的判断,cmOverrideForwardingMethods。

该方法的作用有两个,首先当我们把消息转发到LMCrashDetector之后,由于LMCrashDetector一定也没有该方法,所以还会执行到这里,这样就构成了死循环。

还有一个问题,并不是只有我们会进行消息转发,在我们使用的三方框架中,仍然会有会许多人会交换forwardingTargetForSelector:方法,对于这种情况我们都只需要让它执行原来的forwardingTargetForSelector:方法即可。

所以需要进行如下判断:

- (BOOL)cmOverrideForwardingMethods {
    return (class_getMethodImplementation([NSObject class], @selector(forwardingTargetForSelector:)) !=
                class_getMethodImplementation([self class], @selector(forwardingTargetForSelector:))) || (class_getMethodImplementation([NSObject class], @selector(forwardInvocation:)) != class_getMethodImplementation([self class], @selector(forwardInvocation:)));
}

4.处理崩溃

而对于所有需要转发的情况,我都直接转发的LMCrashDetector类中进行处理,使用依赖注入的方式,将当前的class及SEL传递到处理类中。

/// 崩溃调用者
static Class kCrashInvoker = nil;
/// 崩溃方法
static SEL kCrashSelector = nil;

- (instancetype)initWithInvoker:(Class)invoker selector:(SEL)selector{
    if (self = [super init]) {
        kCrashInvoker = invoker;
        kCrashSelector = selector;
    }
    return self;
}

接下来我们将实现LMCrashDetector的methodSignatureForSelector:和forwardInvocation:两个方法来处理慢速转发。

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"%@-%@-%@",kCrashInvoker,NSStringFromSelector(kCrashSelector),[NSThread callStackSymbols]);
}

将所有需要的信息上报的后台,或者firebase,整个关于调用为实现方法的崩溃拦截功能就实现完成了。

结语

通过本文的介绍,我们深入探讨了如何利用消息转发机制在iOS中实现崩溃拦截。消息转发不仅为未识别的选择器提供了一种优雅的处理方式,也为开发者在调试和优化应用时提供了更多的灵活性。然而,正如我们所讨论的,虽然消息转发能帮助避免崩溃,但滥用它可能会掩盖代码中的真正问题。因此,建议开发者在实际应用中,合理使用这一机制,以保持代码的健壮性和可维护性。希望本文的内容能够为你提供有价值的参考,帮助你更好地掌控iOS开发中的异常处理。
 

  • 8
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值