海神平台是我们自主研发的一个移动端质量监控平台,从去年7月份开始至今,已陆续上线了Crash监控、ANR监控、网络监控、自定义错误等功能,目前已接入了公司内10余款APP(不区分Android和iOS平台)。本文将主要分享Android端在开发Crash监控SDK过程中的一些实践和经验。希望大家能有所收获。
系统提供了一个钩子:Thread.setDefaultUncaughtExceptionHandler;
我们通过设置自定义的UncaughtExceptionHandler,就可以在崩溃发生的时候获取到现场信息。注意,这个钩子是针对单个进程而言的,在多进程的APP中,监控哪个进程,就需要在哪个进程中设置一遍ExceptionHandler。
// Thread.java
public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh)
public interface UncaughtExceptionHandler {
/**
* Method invoked when the given thread terminates due to the
* given uncaught exception.
* Any exception thrown by this method will be ignored by the
* Java Virtual Machine.
* @param t the thread
* @param e the exception
*/
void uncaughtException(Thread t, Throwable e);
}
需要注意的是,在设置ExceptionHandler之前,要先通过get方法将之前的ExceptionHandler进行保存,然后在你消费完这次的崩溃信息后,需将崩溃传递给之前的ExceptionHandler。这样做的目的是在多个监控SDK并存时,每个监控SDK都能侦听到崩溃,系统默认的异常处理是直接退出进程。
为什么这个接口能捕获到Java层的所有崩溃?详见后续文章:《Android端DefaultUncaughtExceptionHandler异常捕获原理解读》
Throwable ex
1)异常类型:ex.getClass().getName(); // 如"java.lang.ArithmeticException"
2)异常信息:ex.getLocalizedMessage(); // 如"divide by zero"
3)堆栈信息:ex.getStackTrace();// StackTraceElement[]
4)异常起因:ex.getCause(); // 创建该Throwable时的构造参数,也是一个Throwable,由此可以组成异常链
Looper.getMainLooper().getThread().getStackTrace();
Thread.currentThread().getStackTrace();
// Thread.getAllStackTraces();
public static Map getAllStackTraces()
public final class StackTraceElement implements java.io.Serializable {
// Normally initialized by VM (public constructor added in 1.5)
private String declaringClass;
private String methodName;
private String fileName;
private int lineNumber;
一般说来,每个StackTraceElement实例都对应着一次函数调用。我们常用的输出异常日志的方法printStackTrace、以及第三方Crash监控工具如Fabric、腾讯Bugly,都是以字符串拼接的方式将数组StackTraceElement[]转换成字符串形式,进行保存、上报或者展示。
如下异常日志样式大家是不是很眼熟?
Fatal Exception:xxxThrowable:xxxMessage
at xxxStackTraceElement11
at xxxStackTraceElement12
at xxx1......
Caused by xxxCauseThrowable:xxxCauseMessage
at xxxStackTraceElement21
at xxxStackTraceElement22
at xxx2......
Caused by xxxCauseCauseThrowable:xxxCauseCauseMessage
at xxxStackTraceElement31
at xxxStackTraceElement32
at xxx3......
没错,这是Fabric上看到的异常详情。它是如何拼接而成的呢?
数据均来自uncaughtException回调接口的入参Throwable e;其中“Fatal Exception:”之后的信息由e本身的className、Message、StackTrace拼接成;随后的“Caused by”数据块的信息由e.getCause()的className、Message、StackTrace拼接成;以此类推。
这里需要注意的是:堆栈的信息长度最长有多长、Cause异常链最多有几层,在线上环境中都是不确定的,Fabric给出的经验值是:
当崩溃发生时,最先要做的就是保存现场数据,并实时上传。
如何实时上传?Fabric是通过ExecutorService加Future.get组成的异步阻塞式方式来实现的。为什么不直接做保存上传等逻辑操作呢?阻塞点在于:Android系统有限定,在主线程进行同步的网络请求操作(所谓同步,就是要等到网络请求结果返回)时,系统会报错:
android.os.NetworkOnMainThreadException
at android.os.StrictMode$AndroidBlockGuardPolicy.onNetwork(StrictMode.java:1460)
改为异步发请求的话,又无法获知上传结果。
除了崩溃时刻的同步上传外,还需要考虑之后的补传逻辑和补传时机,以确保问题最大限度地被记录和发现。
我们经常使用的崩溃指标是:设备/用户崩溃率、会话(session)崩溃率
前者侧重反映了崩溃的影响力,后者侧重反映了崩溃的发生概率。设备或者说用户崩溃率比较好理解,APP端只要尽量保证设备唯一标识的唯一性就可以了。“会话”该怎么理解和定义呢?我们想用“会话”来描述和定义用户的一次使用。Fabric给出的定义是:
所谓Session,就是APP进入前台时刻距离上次退到后台时刻的时间差不小于30秒,则认为是新的会话的开始。
详见:Fabric:session
定义有了,代码上该如何实现呢?Android系统没有提供明确的钩子来获知APP的前后台切换事件,需要综合多个条件自行判断。这里简要讲一下海神Crash SDK的实践,要点如下:
对于混淆后的APP,其崩溃堆栈的信息往往是也是被混淆的,为方便定位和分析,需要做一些辅助工作:
ANR的全拼是Application Not Responding,即程序无响应。当APP在某种情况下不能灵敏地响应用户的操作时,系统就会弹出ANR的对话框。其带给用户的体验伤害仅次于崩溃。发生ANR原因有很多,一方面是手机自身CPU、内存等资源状况不佳或紧张的原因,另一方面是APP存在耗时操作或者存在瞬时内存消耗过大的缺陷。捕获ANR的相关方案网上资料很多,限于篇幅原因,这里直接讲海神的实践。
海神采用的是FileObserver与WatchDog两种方式相结合。其中FileObserver用于Android5.0之前的系统(即低于level 21的系统),其实也可以只采用WatchDog一种方案。
当采用FileObserver方式侦听到/data/anr/traces.txt发生了写操作完毕的事件时,一定是手机发生了ANR。这里要注意两点:
public static ActivityManager.ProcessErrorStateInfo getProcessInANRState(Context context,int totalCounts) {
if (context == null) {
return null;
}
Log.i(TAG,"start find process which in ANR");
ActivityManager activityManager =
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
if (activityManager == null) {
return null;
}
ActivityManager.ProcessErrorStateInfo errorStateInfo;
int i = 0;
do {
List processErrorStateInfoList = activityManager.getProcessesInErrorState();
if (processErrorStateInfoList != null && !processErrorStateInfoList.isEmpty()) {
for (Object process : processErrorStateInfoList) {
errorStateInfo = (ActivityManager.ProcessErrorStateInfo) process;
if (errorStateInfo.condition == 2) {
LJCLog.i("the anr process found!");
return errorStateInfo;
}
}
}
ThreadUtils.sleep(500L);
} while (i++ <= totalCounts);
LJCLog.i("not found process which in ANR!");
return null;
}
此外,还有一个方案大家可以尝试下,就是每次出现ANR弹框前,Native层都会发出signal为SIGNAL_QUIT(值为3)的信号事件。
上一小节讲了如何侦听ANR事件的发生,这一节讲一下如何获取现场的相关信息。ANR的现场信息可以从一下几个地方获取:
数据源 | 可提供内容 | 优缺点 |
---|---|---|
traces.txt | ANR时间、ANR进程、包括UI主线程在内的所有线程的堆栈及Native调用堆栈 | 优点:堆栈数据准确 缺点:解析比较耗时,不适合实时上传 |
ProcessErrorStateInfo | ANR进程、 shortMsg:发生ANR的位置; longMsg:发生ANR的位置、简短原因、当时CPU、内存等占用情况 |
优点:能提供明确的ANR原因和问题代码的所在行;毫秒级获取; 缺点:没有堆栈信息; |
当时当刻的堆栈信息 | 主线程的Java层堆栈、其他线程的Java层堆栈 | 优点:数据毫秒级获取; 缺点:无法获知ANR发生的真正时刻,只是近似 |
海神SDK目前是综合了ProcessErrorStateInfo和出现ANR时的堆栈信息,做到了ANR的实时上传。
以上就是本次总结的全部内容,如有疑问欢迎私聊。