iOS线上防Crash处理并上传未发生的崩溃日志,降低线上APP崩溃率

     线上APP的崩溃率一直是衡量APP用户体验的重要条件之一,所以,我们很有必要做一些安全防护,让APP尽可能少的产生Crash,提高用户体验。在以前的项目中零零散散做过一些防护,这次专门为平台封装了一个Pod库,供各个业务线直接引用,降低线上APP崩溃率,并将错误信息上传到服务器进行分析。

     其实,在开发过程中我们通过设置Xcode配置项、代码里边做判断、加宏编译等可以发现和避免很多Crash,如代码里边使用respondsToSelector、@available做系统判断等,这个库做的防护主要是代码在运行过程中动态产生的Crash,比如接口返回的数据类型有问题导致产生unrecognized selector、NSString和NSArray越界、KVC和NSDictionary访问key值和设置value值等问题。

     GitHub上也有一些封装好的三方库,但是,由于长时间不进行维护了,在新的系统和机型上可能会产生问题,同时,随着iOS开发语言和系统的更新,之前一些容易产生Crash的问题也修复了,所以没必要在进行防护了,比如对象dealloc之后NSNotification还存在会导致崩溃的问题,以及KVO的一些崩溃问题等。

     在这里我先列举一些常见的Crash问题,然后分类型进行分析,列举最新的防护手段(因为有的已经不需要进行防护了):

1、低系统使用高系统API产生的Crash。

2、unrecognized selector类型的Crash,尤其接口返回数据类型有匹配的情况下产生的。

3、NSString、NSMutableString及相关类簇产生的Crash。

4、NSArray、NSMutableArray及相关类簇产生的Crash。

5、NSDictionary、NSMutableDictionary及相关类簇产生的Crash。

6、使用KVC产生Crash。

7、KVO相关Crash。

8、NSNotification相关Crash。

9、NSTimer Crash。

10、野指针 Crash。

11、非主线程刷新UI 导致的Crash。

 

防护手段:

1、低系统使用高系统API产生的Crash:

这个在Xcode中进入工程的Build Settings页面,在“Other C Flags”和“Other C++ Flags”中增加“-Wunguarded-availablility”,设置好之后,如果误调用了高版本API,Clang会检测到并报出警告。

2、unrecognized selector类型的Crash:

方法找不到这种Crash,在崩溃日志中可以说占了很大的比重,尤其是接口返回的数据没有按约定的类型返回或者返回了一个NSNull对象极易产生崩溃。

解决办法:通过runtime来hook NSObject的方法,在自己的方法里边加入try catch去调用原始的方法,这里利用runtime消息转发三部曲机制来处理,这里思考一下,三个方法中我们应该选择哪个方法去做防护呢?第一个方法resolveInstanceMethod中动态增加方法不太合适,第二个方法forwardingTargetForSelector将消息转发给别的对象其实是可以的,但是如果在这个方法中实现,团队中有人想让消息转发走到第三步(forwardInvocation方法)在第三步中做自己的处理,那就会产生问题,因为根本不会走到第三步就被拦截了。所以,为了尽量减少对项目产生侵入性,我选择在第三步对forwardInvocation和methodSignatureForSelector方法做处理来截获将要产生的异常,虽然第三步开销大一些吧。

注意:这里不能对所有的NSObject类都进行这个防护,因为经过测试发现,系统的一些UIView类会在消息转发第三步做自己的处理,比如UIKeyboard相关的。目前,我主要是对@[@"NSNull",@"NSNumber",@"NSString",@"NSDictionary",@"NSArray",@"NSSet"] 这些OC中基础类做了处理。

那对于实现就很简单了,通过hook NSObject的forwardInvocation实例方法和methodSignatureForSelector实例方法,在hook的methodSignatureForSelector方法中构造一个自定义的方法签名,在hook的forwardInvocation方法中加上try catch防护来拦截异常,大概的代码如下:

- (NSMethodSignature *)avoidCrashMethodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature *ms = [self avoidCrashMethodSignatureForSelector:aSelector];
    if (ms == nil) {
        //只对预设置的类构造NSMethodSignature
        for (NSString *classStr in classNameArray) {
            if ([self isKindOfClass:NSClassFromString(classStr)]) {
                //emptyMethod方法是空实现,主要是为了构造一个方法签名
                ms = [AvoidCrashTools instanceMethodSignatureForSelector:@selector(emptyMethod)];
                break;
            }
        }
    }
    return ms;
}

- (void)avoidCrashForwardInvocation:(NSInvocation *)anInvocation {
    @try {
        [self avoidCrashForwardInvocation:anInvocation];
    } @catch (NSException *exception) {
        //解析堆栈,上传错误信息
        [AvoidCrashTools noteErrorWithException:exception type:TypeUnrecognizedSelector];
    } @finally {
    }
}

 

3、NSString、NSMutableString相关方法的Crash:

这里需要注意一下,由于类簇的存在,在hook相关方法的时候,需要考虑类簇,将类簇的相关方法也要hook,比如__NSCFConstantString、 NSTaggedPointerString、 __NSCFString等。我目前防护的相关方法有:

1. - (unichar)characterAtIndex:(NSUInteger)index
2. - (NSString *)substringFromIndex:(NSUInteger)from
3. - (NSString *)substringToIndex:(NSUInteger)to {
4. - (NSString *)substringWithRange:(NSRange)range {
5. - (NSString *)stringByReplacingOccurrencesOfString:(NSString *)target withString:(NSString *)replacement
6. - (NSString *)stringByReplacingOccurrencesOfString:(NSString *)target withString:(NSString *)replacement options:(NSStringCompareOptions)options range:(NSRange)searchRange
7. - (NSString *)stringByReplacingCharactersInRange:(NSRange)range withString:(NSString *)replacement

NSMutableString防护的方法有:

由于NSMutableString是继承于NSString,所以这里和NSString有些同样的方法就不重复写了
1. - (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)aString
2. - (void)insertString:(NSString *)aString atIndex:(NSUInteger)loc
3. - (void)deleteCharactersInRange:(NSRange)range

 

4、NSArray、NSMutableArray及相关类簇产生的Crash。

5、NSDictionary、NSMutableDictionary及相关类簇产生的Crash。

4和5 容器类的防护,没啥可说的,和3中NSString的防护是类似的,具体要hook的方法自己进行选择就可以了,注意进行自测。

 

6、使用KVC产生Crash:

使用KVC访问或设置不存在的key或者访问和设置nil等会产生Crash。所以需要进行防护,防护的方法有:

1、setValue:forKey:
2、setValue:forKeyPath:
3、setValue:forUndefinedKey:
4、setValuesForKeysWithDictionary:

5、valueForKey
6、valueForKeyPath
7、valueForUndefinedKey

7、KVO相关Crash:

现在当被观察者dealloc的时候还被监听着,并不会产生Crash。但是,重复移除观察者或者KVO注册观察者与移除观察者不匹配还是会产生Crash的,不过这两种情景在开发过程中很容易就被发现了,所以,没有必要再做防护了。

8、NSNotification相关Crash:

在iOS9之前当一个对象添加了notification之后,如果dealloc的时候,仍然持有notification,就会出现NSNotification类型的crash。苹果在iOS9之后专门针对于这种情况做了处理,所以在iOS9之后,即使开发者没有移除observer,Notification crash也不会再产生了。如果APP目前从iOS9开始适配的话,NSNotification的Crash也是可以忽略了。

9、NSTimer Crash:

在日常开发中大家会经常使用到NSTimer,但使用NSTimer的 scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:接口做重复性的定时任务时存在一个问题:NSTimer会强引用target实例,所以需要在合适的时机invalidate定时器,否则就会由于定时器timer强引用target的关系导致target不能被释放,造成内存泄露,甚至在定时任务触发时导致crash。 crash的展现形式和具体的target执行的selector有关。

关于NSTimer的防Crash和防循环引用,可以用一个NSProxy对象来做桥接,弱引用原来的target,把这个NSProxy对象当做NSTimer的第一个参数传进去,当调用方法的时候还是让原来的target去调用。这样NSTimer强持有NSProxy对象,但是NSProxy对象是弱持有原来的target,所以就解除循环引用了。可以参考YYText中的YYTextWeakProxy类的设计。

10、野指针 Crash:

在目前的ARC时代,发生野指针的情况很少见了,所以没必要专门做野指针相关的防护了。我们可以设置Xcode的配置,在开发过程中自动检测是否有野指针的情况存在,即利用僵尸对象(Zombie Objects)检测工具来检测,设置如下:

11、非主线程刷新UI 导致的Crash:

由于UIKit是非线程安全的,在子线程刷新UI的相关操作可能会引起Crash,这个我们也可以通过配置Xcode环境变量来在开发的过程中发现这类问题,配置如下,在Edit Scheme中勾选Main Thread Checker 即可:

 

至此,上边列举的常见Crash都有了预防方案,这些方案需要根据自己的项目进行选择。

 

这里说明一下,当本来会产生的Crash被我们拦截之后,我们需要上传这些信息到我们的服务器,这样才能发现这些问题并及时的修复。所以,当在try catch中产生异常时,需要解析当前堆栈信息,找到崩溃的具体类和具体方法,上传这些异常信息。

解析堆栈信息我参考了别人的代码同时加入了自己的优化代码,大概如下:

+ (void)noteErrorWithException:(NSException *)exception type:(CrashType)type {
    //堆栈数据
    NSArray *callStackSymbolsArr = [NSThread callStackSymbols];
    
    //获取在哪个类的哪个方法中实例化的数组  字符串格式 -[类名 方法名]  或者 +[类名 方法名]
    NSString *mainCallStackSymbolMsg = [AvoidCrashTools getMainCallStackSymbolMessageWithCallStackSymbols:callStackSymbolsArr];
    if (mainCallStackSymbolMsg == nil) {
        //没有找到具体的崩溃地址,则默认上传前10条堆栈信息
        NSInteger max = callStackSymbolsArr.count >= 10 ? 10 : callStackSymbolsArr.count;
        NSMutableString *muStr = [NSMutableString string];
        for (int i = 0; i < max; i++) {
            [muStr appendString:callStackSymbolsArr[i]];
        }
        mainCallStackSymbolMsg = muStr;
    }
        
    NSMutableDictionary *infoDic = [NSMutableDictionary dictionary];
    [infoDic setValue:exception.name forKey:@"errorName"];
    [infoDic setValue:[AvoidCrashTools typeStrWithCrashType:type] forKey:@"crashType"];
    [infoDic setValue:exception.reason forKey:@"errorReason"];
    [infoDic setValue:mainCallStackSymbolMsg forKey:@"errorPlace"];

    AvoidCrashLog(@"Crash = %@",infoDic);
    
    //将错误信息放在字典里,用通知的形式发送出去
    dispatch_async(dispatch_get_main_queue(), ^{
        [[NSNotificationCenter defaultCenter] postNotificationName:AvoidCrashNotification object:infoDic userInfo:nil];
    });
}

/**
 *  获取Crash的具体类和方法<根据正则表达式匹配出来>
 *
 *  @param callStackSymbols 堆栈主要崩溃信息
 *
 *  @return Crash的地方
 */
+ (NSString *)getMainCallStackSymbolMessageWithCallStackSymbols:(NSArray<NSString *> *)callStackSymbols {
    //mainCallStackSymbolMsg的格式为   +[类名 方法名]  或者 -[类名 方法名]
    __block NSString *mainCallStackSymbolMsg = nil;
    
    //匹配出来的格式为 +[类名 方法名]  或者 -[类名 方法名]
    NSString *regularExpStr = @"[-\\+]\\[.+\\]";
    NSRegularExpression *regularExp = [[NSRegularExpression alloc] initWithPattern:regularExpStr options:NSRegularExpressionCaseInsensitive error:nil];
    
    for (int index = 0; index < callStackSymbols.count; index++) {
        NSString *callStackSymbol = callStackSymbols[index];
        [regularExp enumerateMatchesInString:callStackSymbol options:NSMatchingReportProgress range:NSMakeRange(0, callStackSymbol.length) usingBlock:^(NSTextCheckingResult * _Nullable result, NSMatchingFlags flags, BOOL * _Nonnull stop) {
            if (result) {
                NSString* tempCallStackSymbolMsg = [callStackSymbol substringWithRange:result.range];
                //get className
                NSString *className = [tempCallStackSymbolMsg componentsSeparatedByString:@" "].firstObject;
                className = [className componentsSeparatedByString:@"["].lastObject;
                NSBundle *bundle = [NSBundle bundleForClass:NSClassFromString(className)];
                //filter category and system class
                if (![className hasSuffix:@")"] && bundle == [NSBundle mainBundle]) {
                    mainCallStackSymbolMsg = tempCallStackSymbolMsg;
                }
                *stop = YES;
            }
        }];
        //除去AvoidCrash本身的类
        if (mainCallStackSymbolMsg.length && ![mainCallStackSymbolMsg containsString:@"AvoidCrash"]) {
            break;
        }
    }
    return mainCallStackSymbolMsg;
}

+ (NSString *)typeStrWithCrashType:(CrashType)type {
    NSString *typeStr;
    switch (type) {
        case TypeUnrecognizedSelector:
            typeStr = @"Crash_UnrecognizedSelector";
            break;
        case TypeKVC:
            typeStr = @"Crash_KVC";
            break;
        case TypeNSString:
            typeStr = @"Crash_NSString";
            break;
        case TypeNSMutableString:
            typeStr = @"Crash_NSMutableString";
            break;
        default:
            break;
    }
    return typeStr;
}

 

解析完堆栈,拼装这些异常信息,最终得到如下的崩溃信息,之后进行上传就可以了:

注意:如果这里errorPlace(崩溃的具体位置)解析不到,则默认会上传前10条堆栈信息,之后从服务器拿到这里的堆栈信息后,配合dsym文件自己进行解析就可以了。

info = {
    crashType = "Crash_UnrecognizedSelector";
    errorName = NSInvalidArgumentException;
    errorPlace = "-[ViewController viewDidLoad]";
    errorReason = "-[__NSCFNumber count]: unrecognized selector sent to instance 0xbd806617e7edf3ae";
}

 

至此,这个库基本就完成了,下面在项目中调用即可:

#ifndef DEBUG
    //开启防Crash处理
    [[AvoidCrash sharedInstance] openAllAvoidCrash];

    //info里边包含发生的崩溃的原因,崩溃的类型和崩溃的具体位置
    [AvoidCrash sharedInstance].reportBlock = ^(NSDictionary * _Nullable info) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            //在这里将拦截的异常上报服务器
            ...
        });
    };
#endif

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值