海神平台是我们自主研发的一个移动端质量监控平台,从去年 7 月份开始至今,已陆续上线了 Crash 监控、ANR 监控、网络监控、自定义错误等功能,目前已接入了公司内 10 余款 APP(不区分 Android 和 iOS 平台)。本文将主要分享 Android 端在开发 Crash 监控 SDK 过程中的一些实践和经验。希望大家能有所收获。
一、Java 层异常捕获
系统提供了一个钩子:
Thread.setDefaultUncaughtExceptionHandler;我们通过设置自定义的 UncaughtExceptionHandler,就可以在崩溃发生的时候获取到现场信息。注意,这个钩子是针对单个进程而言的,在多进程的 APP 中,监控哪个进程,就需要在哪个进程中设置一遍 ExceptionHandler。
需要注意的是,在设置 ExceptionHandler 之前,要先通过 get 方法将之前的 ExceptionHandler 进行保存,然后在你消费完这次的崩溃信息后,需将崩溃传递给之前的 ExceptionHandler。这样做的目的是在多个监控 SDK 并存时,每个监控 SDK 都能侦听到崩溃,系统默认的异常处理是直接退出进程。
二、堆栈数据来源
2.1 从 Throwable 中我们可以获取以下信息:
2.2 主线程的堆栈信息:
2.3 当前线程的堆栈信息:
2.4 全部线程的堆栈信息:
三、堆栈信息处理
一般说来,每个 StackTraceElement 实例都对应着一次函数调用。我们常用的输出异常日志的方法 printStackTrace、以及第三方 Crash 监控工具如 Fabric、腾讯 Bugly,都是以字符串拼接的方式将数组 StackTraceElement[]转换成字符串形式,进行保存、上报或者展示。
如下异常日志样式大家是不是很眼熟?
没错,这是 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 系统有限定,在主线程进行同步的网络请求操作(所谓同步,就是要等到网络请求结果返回)时,系统会报错:
改为异步发请求的话,又无法获知上传结果。除了崩溃时刻的同步上传外,还需要考虑之后的补传逻辑和补传时机,以确保问题最大限度地被记录和发现。
五、关于崩溃率
我们经常使用的崩溃指标是:设备/用户崩溃率、会话(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)。
代码实现如下:
此外,还有一个方案大家可以尝试下,就是每次出现 ANR 弹框前,Native 层都会发出 signal 为 SIGNAL_QUIT(值为 3)的信号事件。
八、ANR 的现场信息
上一小节讲了如何侦听 ANR 事件的发生,这一节讲一下如何获取现场的相关信息。ANR 的现场信息可以从以下几个地方获取:
1)traces.txt;
2)ProcessErrorStateInfo 实例;
3)当时当刻的堆栈信息,获取方式见第二小节。
三者的优缺点对比:
海神 SDK 目前是综合了 ProcessErrorStateInfo 和出现 ANR 时的堆栈信息,做到了 ANR 的实时上传。
作者介绍:
伏牛(企业代号名),目前负责贝壳移动端业务架构组 Android 基础库开发相关工作。
本文转载自公众号贝壳产品技术(ID:gh_9afeb423f390)。
原文链接:
https://mp.weixin.qq.com/s/PoWPWy3cXFlG1nohgJTJgw
评论