点击围观!腾讯 TAPD 助力金融行业研发提效、敏捷转型最佳实践! 了解详情
写点什么

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

  • 2019-09-27
  • 本文字数:4309 字

    阅读完需:约 14 分钟

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

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

一、Java 层异常捕获

系统提供了一个钩子:


Thread.setDefaultUncaughtExceptionHandler;我们通过设置自定义的 UncaughtExceptionHandler,就可以在崩溃发生的时候获取到现场信息。注意,这个钩子是针对单个进程而言的,在多进程的 APP 中,监控哪个进程,就需要在哪个进程中设置一遍 ExceptionHandler。


// Thread.javapublic 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 ex1)异常类型: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:xxxMessageat xxxStackTraceElement11at xxxStackTraceElement12at xxx1......Caused by xxxCauseThrowable:xxxCauseMessageat xxxStackTraceElement21at xxxStackTraceElement22at xxx2......Caused by xxxCauseCauseThrowable:xxxCauseCauseMessageat xxxStackTraceElement31at xxxStackTraceElement32at 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)当时当刻的堆栈信息,获取方式见第二小节。


三者的优缺点对比:



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


作者介绍:


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


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


原文链接:


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


2019-09-27 13:14906

评论

发布
暂无评论
发现更多内容

肝下50万字的《Linux内核精通》笔记,你的底层原理水平将从入门到入魔【建议收藏】

Linux爱好者

内存管理 嵌入式 Linux内核 进程管理 驱动开发

阿里大佬手码的SpringCloud+Alibaba笔记开源了,堪称保姆式教学

Geek_0c76c3

Java 数据库 开源 程序员 开发

【高并发】ScheduledThreadPoolExecutor与Timer的区别和简单示例

冰河

并发编程 多线程 高并发 协程 异步编程

TDengine | taosdump的使用方法和注意事项

TDengine

数据库 tdengine 开源 时序数据库 taosdump

大型企业选择低代码的主要原因是什么?

优秀

低代码 低代码平台

数据中台建设5大关键步骤

阿泽🧸

数据中台 10月月更

【导航】ESP32-C3 入门教程目录 【快速跳转】

矜辰所致

目录 ESP32-C3 10月月更

真的香!Github一夜爆火被各大厂要求直接下架的面试题库也太全了

Geek_0c76c3

Java 数据库 开源 程序员 开发

在数字化浪潮中,为企业建造一艘“方舟”

元年技术洞察

微服务 云原生 企业数字化 PaaS 平台

金三银四跳槽季,美团、字节、阿里、腾讯Java面经,终入字节

Geek_0c76c3

Java 数据库 开源 程序员 开发

vue组件通信方式有哪些?

bb_xiaxia1998

Vue

Java后端没这些东西都不敢跳!对标阿里P7技术路线你值得拥有

Geek_0c76c3

Java 数据库 程序员 架构 开发

精彩演讲推荐|智能化变更防控方法、架构与组织实践

TRaaS

阿里资深架构师把大厂高频 2000+ 道 Java 面试题全部总结出来了,分分钟拿捏面试官

Geek_0c76c3

Java 数据库 开源 程序员 开发

大杀四方!腾讯强推599页Netty进阶神技,彻底解析Netty

Geek_0c76c3

Java 数据库 程序员 架构 开发

学了阿里大佬的 SpringCloud微服务项目真香!即刻涨薪35K

Geek_0c76c3

Java 开源 程序员 架构 面试

短期内跳槽的Java程序员必看的八项知识点+两大项目实战

Geek_0c76c3

Java 数据库 开源 程序员 开发

ConcurrentDictionary<T,V> 的这两个操作不是原子性的

有态度的马甲

双活数据中心建设要点

穿过生命散发芬芳

10月月更 双数据中心

太牛了,这份Spring Cloud Alibaba学习文档清晰全面,一应俱全

小二,上酒上酒

spring Spring Cloud

太全了!华为大神珍藏版SpringBoot全优笔记,首次分享

Geek_0c76c3

Java 数据库 开源 程序员 架构

Java:锁定 Excel 中的特定单元格

Geek_249eec

Java Excel 单元格

【Go】Go 操作 excel 代码封装

非晓为骁

Excel go语言

vue组件通信6种方式总结(常问知识点)

bb_xiaxia1998

Vue

Koordinator v0.7: 为任务调度领域注入新活力

阿里巴巴云原生

阿里云 云原生 Koordinator

SAP | 在abap开发过程中常用的Tcode

暮春零贰

SAP abap 10月月更

字节奋战8年,回头一看只剩下这份1857页的算法笔记了

Geek_0c76c3

Java 数据库 开源 程序员 开发

见大牛、聊感悟、拿好礼...开发者一起来微软Ignite赴约!

InfoQ写作社区官方

热门活动

全网独家首发Java面试题,包含Spring全家桶+高并发+Netty+Redis+Dubbo等面试专题

小二,上酒上酒

Java Linux Netty 高并发 Spring全家桶

2022全网独一份Java面试题整理,包含30个技术栈, 1575 道Java 架构师面试题

Geek_0c76c3

Java 数据库 开源 程序员 开发

进击的PyTorch,和它背后的开源领袖

OneFlow

人工智能 机器学习 深度学习 开源

海神平台Crash监控SDK(Android)开发经验总结_文化 & 方法_伏牛_InfoQ精选文章