在开发APP的过程中,崩溃等异常总是让我们不堪其烦,不过开发阶段的崩溃等问题,都是小事,可以进行处理,但是一旦发布的版本出现崩溃的问题,那就是大问题,不仅要连夜更新版本,还要为找这个bug不断的尝试,所以,我总结了看到的一些对于崩溃等异常的处理,以备自己日后参考。
若有可以改进的,望各位大大不吝赐教。
(注:本文只介绍使用方法,不对原理做深层解析,小弟也没这水平,接下来闲下来了,在研究代码的实现原理,出一篇原理篇~)
一、关于崩溃
闪退估计是我们最不想看到的,对于用户而言,马上就能产生一种不悦,对于投资方而言,也会产生对技术实力的不信任感,所以,我们就需要对闪退进行处理,这里介绍一个不错的三方:AvoidCrash
,写这个的大大也很牛逼,原文参照这里。
这个三方可以处理例如插入空值到字典中或数组中引起的崩溃、数组越界引起的崩溃、unrecognized selector sent to instance
等等的崩溃,都能捕获并且避免闪退。
对于插入空值、越界等,原理比较简单,就是利用Runtime
的方法交换,把普通的插入和取值的方法,替换成安全插入和安全读取的方法,具体代码可以去看源码。
话不多说,先上效果:
以下是可导致崩溃的代码:
NSString *nilStr = nil;
NSArray *array = @[@"chenfanfang", nilStr];
若有AvoidCrash来防止崩溃,则不会崩溃,并且会将原本会崩溃情况的详细信息打印出来,如下图:
效果不错吧,接下来上使用步骤:
集成:
建议使用cocoapod
,仅需要pod AvoidCrash
一句话即可。(手动导入的步骤,可以参照上面所说的原文)。使用方法:(只要在
AppDelegate
的didFinishLaunchingWithOptions
方法中调用avoidCrash
方法,就可以开始监听异常。)
- (void)avoidCrash {
/*
* 项目初期不需要对"unrecognized selector sent to instance"错误进行处理,因为还没有相关的崩溃的类
* 后期出现后,再使用makeAllEffective方法,把所有对应崩溃的类添加到数组中,避免崩溃
* 对于正式线可以启用该方法,测试线建议关闭该方法
*/
[AvoidCrash becomeEffective];
// [AvoidCrash makeAllEffective];
// NSArray *noneSelClassStrings = @[
// @"NSString"
// ];
// [AvoidCrash setupNoneSelClassStringsArr:noneSelClassStrings];
//监听通知:AvoidCrashNotification, 获取AvoidCrash捕获的崩溃日志的详细信息
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(dealwithCrashMessage:) name:AvoidCrashNotification object:nil];
}
- 再监听异常的通知:
- (void)dealwithCrashMessage:(NSNotification *)notification {
MYLog(@"\n🚫\n🚫监测到崩溃信息🚫\n🚫\n");
/*
* 在这边对避免的异常进行一些处理,比如上传到日志服务器等。
*/
}
以上就是避免崩溃的简单用法,关于能处理哪些异常,可以自行查看Git中的项目介绍。
二、关于异常的统计
上述的方法,能够避免崩溃,但是不能够避免所有状况的崩溃,作者也在不断的根据用户的使用情况进行更新,尽量对所有已知的崩溃进行避免。所以,我们还需要对异常进行其他的收集,也能有效的帮助自己改进APP。
这里仅做腾讯的Bugly进行介绍,因为其他的例如友盟、极光的,个人感觉都没有Bugly好用,我也就不做介绍了,有兴趣的可以自行了解。
这里参照的文章原文在此。
- 集成
集成很简单,按照官方文档来就好,我们这里建个简单的小项目,模拟一些崩溃,测试下Bugly的bug上报及时性。
项目就取名叫NSException
了,创建好项目后,去Bugly的控制台,添加我们的应用。
创建完应用,进入下一个界面,我们选择异常上报。
再到我们的APP的appDelegate
中didFinishLaunchingWithOptions
方法内调用初始化方法即可。
// 头文件
#import <Bugly/Bugly.h>
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[Bugly startWithAppId:@"此处替换为你的AppId"];
return YES;
}
AppID可以点击你在控制台创建的App,然后点产品设置就能看到了。
- Bug上传测试
接下来我们在ViewConroller中随便创造一个闪退的bug
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSArray *arr = @[@"", @""];
arr[5];
}
运行,崩溃,刷新Bugly的控制台,你会发现,bug已经统计到了。所以,Bugly的崩溃上传是在崩溃后立刻上传的。
我们点进异常问题中去看一下,崩溃信息大致是这样的,我们可以很直观的看到崩在哪个方法里了。
进阶
如果我们就这样使用Bugly是不是太可惜了,我们来看看Bugly还有什么功能;查看头文件,会发现Bugly有三个类暴露出来,分别是Bugly
、BuglyConfig
和BuglyLog
。
1.BuglyConfig
类主要用于个性话配置Bugly类,由一些属性和BuglyDelegate
代理组成。
- 属性:
BuglyConfig
大部分属性有设有默认值,一般不用更改,但是关于卡顿监控的属性确是默认关闭的:
/**
* 卡顿监控开关,默认关闭
*/
@property (nonatomic) BOOL blockMonitorEnable;
/**
* 卡顿监控判断间隔,单位为秒
*/
@property (nonatomic) NSTimeInterval blockMonitorTimeout;
如果需要上报卡顿,只需要将blockMonitorEnable
设为true
,给blockMonitorTimeout
设置一个合理的值即可;
- 代理:
BuglyConfig
可以设置一个代理,来自定义上传崩溃的附属信息;
@protocol BuglyDelegate <NSObject>
@optional
/**
* 发生异常时回调
* @param exception 异常信息
* @return 返回需上报记录,随异常上报一起上报
*/
- (NSString * BLY_NULLABLE)attachmentForException:(NSException * BLY_NULLABLE)exception;
@end
我们的初始化就改成:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
BuglyConfig *config = [[BuglyConfig alloc] init];
//监听卡顿
config.blockMonitorEnable = YES;
config.blockMonitorTimeout = 3;
config.consolelogEnable = YES;
config.delegate = self;
[Bugly startWithAppId:@"此处替换为你的AppId" config:config];
// [self avoidCrash];
return YES;
}
- (NSString *)attachmentForException:(NSException *)exception {
NSLog(@"异常事件代理");
return [NSString stringWithFormat:@"TEST: %@",exception.userInfo];
}
再运行一次,崩溃,然后我们看看Bugly控制台上报记录:
文件内部记录的就是异常的代理方法所上报的内容。
2.上传打印日志,BuglyLog
类主要用于打印日志,有6种级别:
typedef NS_ENUM(NSUInteger, BuglyLogLevel) {
BuglyLogLevelSilent = 0,
BuglyLogLevelError = 1,
BuglyLogLevelWarn = 2,
BuglyLogLevelInfo = 3,
BuglyLogLevelDebug = 4,
BuglyLogLevelVerbose = 5,
};
BuglyLog除了控制台打印,还有一个重要功能就是上报打印内容,内容将在崩溃时一同被上报;但是这个功能是默认不上报的,需要配置BuglyConfig的reportLogLevel属性;如config.reportLogLevel = BuglyLogLevelWarn,将会上报BuglyLogLevelWarn和BuglyLogLevelError级别的打印日志。
3.自定义上报异常,我们再回到Bugly类,除了初始化Bugly的方法外,还有一些其他的方法:
自定义上报错误
/**
* 上报自定义异常
* @param exception 异常信息
*/
+ (void)reportException:(nonnull NSException *)exception;
/**
* 上报错误
* @param error 错误信息
*/
+ (void)reportError:(NSError *)error;
重点来了!!!!
配合上AvoidCrash,使用上报自定义异常方法,我们就既能避免崩溃,又能监听异常!
//AvoidCrash异常通知监听方法,在这里我们可以调用reportException方法进行上报
- (void)dealwithCrashMessage:(NSNotification *)notification {
NSLog(@"\n🚫\n🚫监测到崩溃信息🚫\n🚫\n");
NSException *exception = [NSException exceptionWithName:@"AvoidCrash" reason:[notification valueForKeyPath:@"userInfo.errorName"] userInfo:notification.userInfo];
[Bugly reportException:exception];
}
以上就是AvoidCrash+Bugly优化APP的运行处理。
虽然Bugly的崩溃列表中我们能看到得到代码的崩溃信息,但想更具体的分析代码位置,就要用到符号表了。
三、符号表
没有符号表,我们就无法定位崩溃中的符号对应的代码所在的类以及类中的行数位置。我们在每次构建版本、debug的时候,都会生成dSYM后缀名的符号表文件,而我们App在手机上运行的时候,崩溃后产生的崩溃信息,不可能定位到代码的多少多少行,因为这些信息对于App运行是没有意义的,存储在App中势必会增大安装包的体积,所以App的崩溃信息都是存储为各种符号,具体符号代表什么,需要去符号表中查找对应的含义。
我们每次debug、构建版本,都会生成dSYM文件,都对应了一个UUID(像我们的手机一样,都有一个唯一标志),按下图指示,我们就能找到我们所使用的App版本对应的dSYM文件的UUID,通过这个UUID,我们就能找到存储在我们电脑中的dSYM文件,将这个文件上传到bugly,bugly会自动帮我们找到崩溃符号的含义。
需要注意的是,构建版本会自动生成dSYM文件,但debug的时候,是没有的,需要我们手动开启。在build setting中搜索debug,将下面两项内容修改为正确的设置:
有了符号表的UUID,我们打开终端,按UUID找到符号表的路径。
mdfind "com_apple_xcode_dsym_uuids == A8E87810-70A7-3335-B638-C8B01BE15D79"
后面的一串字母数字组合,就是我们的UUID,这里需要将UUID按一定格式处理下,也就是在特定位置插入“-”,具体格式如下:
来到终端,运行上面的命令,就定位到了dSYM文件的位置:
打开文件路径,就找到了dSYM文件:
拷贝出来,压缩为zip文件,上传到bugly上。
刷新页面,再回去看刚才的问题,定位到了为ViewController.m的第24行:
这样我们就定位到了有问题的地方。
官网文档也提供了自动上传dSYM文件的操作流程,有兴趣的可以试试,避免以后每次新版本都要手动上传dSYM文件。
dSYM文件也可以手动查找:
找到你构建的版本,右键show in finder:
然后在定位到的文件上右键显示包内容就OK了:
总结
以上就是Bugly收集异常的过程,由于我也只是刚刚接触Bugly,所以自己也有几个问题没有解决,例如对于Bugly的符号表的dSYM文件的上传,每次新版本dSYM文件都会改变?那手动是有点麻烦,自动的方法也得去看看。
还有很多需要深入学习的,我也会继续学习继续分享,同样的,希望各位大大能够指出一些可以改进的或者理解有误的,帮助小弟进步,例如AvoidCrash作者所说的“一些处理”,有的话万分感激。