海神平台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都能侦听到崩溃,系统默认的异常处理是直接退出进程。
为什么这个接口能捕获到Java层的所有崩溃?详见后续文章:《Android端DefaultUncaughtExceptionHandler异常捕获原理解读》

二、堆栈数据来源

  • 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秒,则认为是新的会话的开始。

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

  • 1)基于APP全局的ActivityLifecycleCallbacks进行页面生命周期的监听,在发生“OnStopped”事件时,判断一下当前APP是否是前台应用,若不是,则认为此刻APP要退到后台了,记下时间戳;当发生“OnStarted”事件时,计算下两次事件的时间差是否超过30秒,若是,则本次是新会话的开始,需要更新会话Id值。如何判断APP是否是前台应用,网上资料比较多,这里不展开讲。
  • 2)产生新会话的条件有三种:一是1)中的前后台切换;二是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)当时当刻的堆栈信息,获取方式见第二小节。
    三者的优缺点对比:
数据源可提供内容优缺点
traces.txtANR时间、ANR进程、包括UI主线程在内的所有线程的堆栈及Native调用堆栈优点:堆栈数据准确
缺点:解析比较耗时,不适合实时上传
ProcessErrorStateInfoANR进程、
shortMsg:发生ANR的位置;
longMsg:发生ANR的位置、简短原因、当时CPU、内存等占用情况
优点:能提供明确的ANR原因和问题代码的所在行;毫秒级获取;
缺点:没有堆栈信息;
当时当刻的堆栈信息主线程的Java层堆栈、其他线程的Java层堆栈优点:数据毫秒级获取;
缺点:无法获知ANR发生的真正时刻,只是近似

海神SDK目前是综合了ProcessErrorStateInfo和出现ANR时的堆栈信息,做到了ANR的实时上传。
以上就是本次总结的全部内容,如有疑问欢迎私聊。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值