海神平台 Crash 监控 SDK(Android)开发经验总结

阅读数:71 2019 年 9 月 27 日 13:14

海神平台Crash监控SDK(Android)开发经验总结

海神平台是我们自主研发的一个移动端质量监控平台,从去年 7 月份开始至今,已陆续上线了 Crash 监控、ANR 监控、网络监控、自定义错误等功能,目前已接入了公司内 10 余款 APP(不区分 Android 和 iOS 平台)。本文将主要分享 Android 端在开发 Crash 监控 SDK 过程中的一些实践和经验。希望大家能有所收获。

一、Java 层异常捕获

系统提供了一个钩子:

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.
* <p>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 都能侦听到崩溃,系统默认的异常处理是直接退出进程。

二、堆栈数据来源

2.1 从 Throwable 中我们可以获取以下信息:

复制代码
Throwable ex
1) 异常类型:ex.getClass().getName(); // 如 "java.lang.ArithmeticException"
2) 异常信息:ex.getLocalizedMessage(); // 如 "divide by zero"
3) 堆栈信息:ex.getStackTrace();// StackTraceElement[]
4) 异常起因:ex.getCause(); // 创建该 Throwable 时的构造参数,也是一个 Throwable,由此可以组成异常链

2.2 主线程的堆栈信息:

复制代码
Looper.getMainLooper().getThread().getStackTrace();

2.3 当前线程的堆栈信息:

复制代码
Thread.currentThread().getStackTrace();

2.4 全部线程的堆栈信息:

复制代码
// Thread.getAllStackTraces();
public static Map<Thread, StackTraceElement[]> 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 给出的经验值是:

  • 3.1 每个 Throwable 的堆栈长度,Fabric 限制为 1024 字节
  • 3.2 每个 Throwable 的堆栈里邻近行可能存在重复,可以做一下去重,Fabric 限制为最多连续 10 行重复
  • 3.3 整个异常链需要有长度限制,Fabric 限制为最长 8 层

四、关于上报时机

当崩溃发生时,最先要做的就是保存现场数据,并实时上传。如何实时上传?

Fabric 是通过 ExecutorService 加 Future.get 组成的异步阻塞式方式来实现的。为什么不直接做保存上传等逻辑操作呢?阻塞点在于:Android 系统有限定,在主线程进行同步的网络请求操作(所谓同步,就是要等到网络请求结果返回)时,系统会报错:

复制代码
android.os.NetworkOnMainThreadException
at android.os.StrictMode$AndroidBlockGuardPolicy.onNetwork(StrictMode.java:1460)

改为异步发请求的话,又无法获知上传结果。除了崩溃时刻的同步上传外,还需要考虑之后的补传逻辑和补传时机,以确保问题最大限度地被记录和发现。

五、关于崩溃率

我们经常使用的崩溃指标是:设备 / 用户崩溃率、会话(session)崩溃率。

前者侧重反映了崩溃的影响力,后者侧重反映了崩溃的发生概率。设备或者说用户崩溃率比较好理解,APP 端只要尽量保证设备唯一标识的唯一性就可以了。“会话”该怎么理解和定义呢?我们想用“会话”来描述和定义用户的一次使用。

Fabric 给出的定义是:

所谓 Session,就是 APP 进入前台时刻距离上次退到后台时刻的时间差不小于 30 秒,则认为是新的会话的开始。

定义有了,代码上该如何实现呢?Android 系统没有提供明确的钩子来获知 APP 的前后台切换事件,需要综合多个条件自行判断。这里简要讲一下海神 Crash SDK 的实践,要点如下:

  • 1)基于 APP 全局的 ActivityLifecycleCallbacks 进行页面生命周期的监听,在发生“OnStopped”事件时,判断一下当前 APP 是否是前台应用,若不是,则认为此刻 APP 要退到后台了,记下时间戳;当发生“OnStarted”事件时,计算下两次事件的时间差是否超过 30 秒,若是,则本次是新会话的开始,需要更新会话 Id 值。如何判断 APP 是否是前台应用,网上资料比较多,这里不展开讲。

  • 2)产生新会话的条件有三种:一是中的前后台切换;二是 APP 冷启动;三是发生子进程崩溃。为什么子进程崩溃时要主动更新会话 Id 呢?理由是我们认为在一个会话期间,最多只能发生一次崩溃异常。而子进程崩溃时,APP 通常没有退出,也很可能没有引起页面切换。所以就有必要主动更新会话 Id。

六、关于混淆

对于混淆后的 APP,其崩溃堆栈的信息往往是也是被混淆的,为方便定位和分析,需要做一些辅助工作:

  • 1)每次打包生成混淆 APK 的时候,需要把 Mapping 文件保存并上传到监控后台;

  • 2)海神平台目前的标记方式是使用 appName+versionCode 组合来标记一个 Mapping 文件。如果觉得这种标记粒度还不够细,可以设法标记每一次的打包行为,当发生 Crash 的时候把这个标记 Id 一并上传,以便后端精确匹配到对应的 Mapping 文件。

  • 3)Android 原生的反混淆的工具包是 retrace.jar,在监控后台用来实时解析每个上报的崩溃时,需要对其进行改造。retrace 的原理是将 Mapping 文件进行文本解析和对象实例化,这个过程比较耗时。海神平台的实践是:将 Mapping 对象实例进行了内存缓存,但为了防止内存泄露和内存过多占用,又增加了定期自动回收的逻辑。目前一个崩溃的反混淆耗时在 1 毫秒左右。

七、如何捕获 ANR

ANR 的全拼是 Application Not Responding,即程序无响应。当 APP 在某种情况下不能灵敏地响应用户的操作时,系统就会弹出 ANR 的对话框。其带给用户的体验伤害仅次于崩溃。

发生 ANR 原因有很多,一方面是手机自身 CPU、内存等资源状况不佳或紧张的原因,另一方面是 APP 存在耗时操作或者存在瞬时内存消耗过大的缺陷。捕获 ANR 的相关方案网上资料很多,限于篇幅原因,这里直接讲海神的实践。

海神采用的是 FileObserver 与 WatchDog 两种方式相结合。其中 FileObserver 用于 Android5.0 之前的系统(即低于 level 21 的系统),其实也可以只采用 WatchDog 一种方案。

当采用 FileObserver 方式侦听到 /data/anr/traces.txt 发生了写操作完毕的事件时,一定是手机发生了 ANR。这里要注意两点:

  • 1)写操作完毕的事件,系统会连续发出多次,需要增加相应逻辑来避免重复响应和处理;

  • 2)traces.txt 文件的解析一般会在若干秒甚至十几秒,比较耗时;另外,traces.txt 文件里可能会记录多个进程的信息,其中发生了 ANR 的进程不一定记录在文件开头。而使用 WatchDog 方案监控到的结果,只能说明 APP 发生了 UI 阻塞,未必会 ANR,需要进行二次校验。校验的方式就是等待手机系统出现发生了 Error 的进程,并且 Error 类型是 NOT_RESPONDING(值为 2)。

  • 代码实现如下:

复制代码
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 事件的发生,这一节讲一下如何获取现场的相关信息。ANR 的现场信息可以从以下几个地方获取:

  • 1)traces.txt;

  • 2)ProcessErrorStateInfo 实例;

  • 3)当时当刻的堆栈信息,获取方式见第二小节。

三者的优缺点对比:

海神平台Crash监控SDK(Android)开发经验总结

海神 SDK 目前是综合了 ProcessErrorStateInfo 和出现 ANR 时的堆栈信息,做到了 ANR 的实时上传。

作者介绍:
伏牛 (企业代号名),目前负责贝壳移动端业务架构组 Android 基础库开发相关工作。

本文转载自公众号贝壳产品技术(ID:gh_9afeb423f390)。

原文链接:

https://mp.weixin.qq.com/s/PoWPWy3cXFlG1nohgJTJgw

评论

发布