引言
在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开发中的异常处理。