Android Crash监控SDK设计思路

Crash率是Android App中的重要指标,对用户体验的影响非常大,因此监控一个App的Crash率是一个很重要的事情。第三方有现成的Crash监控方案,但是不一定能满足所有项目的需要,本文讨论Android监控SDK的完整设计思路,包括数据采集,存储,上报,展示,报警,去重等一系列流程。

Crash率的计算

可以按照 Crash率 = Crash次数 / App启动次数 的方式计算。

也可以做一些优化,避免个别极端情况导致Crash率非常不稳定。例如对于单个用户,限制每天计算的Crash次数不超过20次。毕竟,一天崩溃这么多次还在坚持使用的用户,很可能不是真实用户。可能是在对App做逆向分析,或者是爬虫等非正常用户。

信息采集

Java代码中的Crash的采集一般使用Thread中的UncaughtExceptionHandler实现。C++代码、JavaScript代码中的Crash收集相对复杂,可以参考其他博客或者开源项目的实现,这里不做介绍。

为了便于分析Crash原因,实际上采集的信息可能包括:

  • Crash发生时的详细信息,包括完整的堆栈,发生的时间戳,崩溃所在的线程信息,当时的内存消耗等。对于带有cause的Crash,还需要把cause的信息也收集起来。

  • 设备信息,包括Android版本,手机型号,系统ROM版本等。

  • App信息,包括App的版本,渠道等。

  • 用户操作行为和页面跳转路径,便于尝试复现崩溃问题。

  • 用户信息,例如用户账号、手机号,但是要注意保护用户隐私。如果出现实在无法解决并且比较严重的问题,可以尝试联系用户解决。

UncaughtExceptionHandler的使用

一个复杂项目中可能有多个SDK需要使用UncaughtExceptionHandler,因此直接设置Handler可能会清除掉其他SDK的Handler逻辑:

1
2
3
4
5
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
public void uncaughtException(Thread t, Throwable e) {
// ...
}
});

更保险的做法应该是:

1
2
3
4
5
6
7
8
9
final UncaughtExceptionHandler other = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
public void uncaughtException(Thread t, Throwable e) {
// ...
if (other != null) {
other.uncaughtException(t, e);
}
}
});

循环嵌套问题:

如果两个SDK都使用了上述代码逻辑,并且发生了重复初始化,就可能导致:SDK-A调用了SDK-B的Handler,而SDK-B又调用了SDK-A的Handler,于是SDK-A中的代码逻辑被重复执行。如果两个SDK中Handler使用的是单例,则直接形成死循环,导致StackOverflow。

解决方法是搞清楚初始化逻辑,避免重复初始化,以及可以设置标志位避免重复执行。

发生重复初始化,可能是单纯的代码混乱,或者代码合并导致的,也可能原因更复杂。例如SDK之间有互相依赖,例如B依赖A,B初始化时也会初始化A,而用户并不知情,同时调用了A和B的初始化,导致B被重复初始化。

存储与上报策略

Crash发生后,通常应该保存到本地,下次App正常运行时,在后台上报。因为Crash发生后,App进程随时可能推出,如果立即发网络请求上报,很可能会上传失败。

持久化存储,通常可以用SQLite数据库,或者文件。

但是这里又有一个问题,如果崩溃发生在Application.onCreate或者第一个Activity初始化时,并且每次都会发生崩溃,而上报Crash的时机设计的比较晚(例如App启动一分钟后),这个Crash就永远没法被上报了,从而没法发现问题。

可以考虑在崩溃发生时保存,并且同时尝试上报,上报成功了就删除本地数据,上报失败了下次启动时再次尝试上报。

上报

单次上报条数限制:如果“库存”的Crash信息较多,应该限制单次上报的信息,避免消耗过多用户流量,以及过大的网络请求容易中途上传失败。

请求数据压缩、加密:数据应该压缩。如果涉及到敏感信息,还应该做适当的加密。

上报成功后删除:上报成功后,删除本地数据。

展示

反混淆

如果使用了Proguard,上报到后台的崩溃堆栈是被混淆的,不方便查看。

可以在Jenkins上打包APK时,提前保存Proguard生成的Mapping文件,并发送给Crash后台系统。在Crash后台通过上报的APK版本号找到Mapping文件,然后调用Proguard提供的ReTrace命令,将原始堆栈还原出来。

分类

Crash信息应该进行分类,按照App版本、Android系统版本、Crash类型等多种纬度分类。

排序

Crash信息应该可以排序,例如按照Crash发生率排序。

实时报警

如果短时间内发生大量Crash,导致Crash率上涨达到一定程度,可以给相关人员发短信邮件等,自动报警,及时排查问题。

去重问题

为了保证Crash率的统计准确性,理想情况下,应该既能上报所有的Crash信息,又不会重复上报同一个Crash。

确保Crash能上报,前面说了,通过先存储再上报的方式实现。这里说一下去重问题。

1、前文提到的UncaughtExceptionHandler循环嵌套问题。

2、收集Crash的代码需要加try-catch避免再次发生崩溃,收集完成后,调用相关API确保退出进程。

1
2
3
4
5
6
7
8
9
10
public void uncaughtException(Thread t, Throwable e) {
try {
// ...
} catch (Throwable t2) {
// ...
} finally {
Process.killProcess(Process.myPid());
System.exit(10);
}
}

3、上报时,由于网络故障,后台已经收到Request数据,但Response返回出错,导致客户端请求超时。此时客户端认为没有上报成功,但实际上后台已经收到数据。下次客户端还会重复上报。

单纯在客户端避免Crash重复上报不太现实,因此必须配合后台共同实现。实现思路很简单:

  • 每次Crash发送时,客户端给Crash同时生成一个唯一的GUID,保存到本地数据库,上报时也会同步上报。
  • 后台使用GUID去重。