其他
让 nativePollOnce 不再排名第一 | 钉钉 ANR 治理最佳实践
引言
在上篇文章《钉钉 ANR 治理最佳实践 | 定位 ANR 不再雾里看花》中介绍了因为 ANR Trace 刻舟求剑的问题,导致 ANR 监控平台中排名第一的往往都是 nativePollOnce。也说明基于 ANR Trace 里的堆栈进行聚合并不能定位到 App 的头部 ANR 问题。
本文将重点介绍 ANRCanary 的 ANR 归因算法和 ANR 归因聚合上报的能力,帮助研发人员更快的分析和定位头部 ANR 问题。
1. 术语表
2. 其他 ANR 原因
在 App 的运行环境中,主线程并非独立存在的,因此导致 ANR 的原因也不一定都是长耗时主线程任务。接下来聊聊钉钉遇到的其他原因导致 ANR 的两种情况。
2.1 线程死锁检测
线程死锁是导致 ANR 的原因之一。两个子线程发生死锁会产生连锁反应,可能会让主线程进入阻塞状态,从而导致 ANR 。
背景知识
如上图所示,线程死锁的原因,通常是两个或多个线程在锁操作的过程中,出现了循环等待的情况而导致的。
所以关键点是:拿到线程的持有锁和等待锁信息,再配合有向无环图算法,检测是否存在循环依赖,就可以做到死锁检测。
获取线程锁信息
先来看 VMStack:VMStack 源码[1]
/**
* @hide
*/
public final class VMStack {
......
/**
* @hide
*/
@SystemApi(client = MODULE_LIBRARIES)
native public static @Nullable AnnotatedStackTraceElement[] getAnnotatedThreadStackTrace(Thread t);
......
}
系统隐藏类 VMStack#getAnnotatedThreadStackTrace()
接口详细定义如上图所示,基于该接口可以获取线程的 AnnotatedStackTraceElement[]
。
接下来,再看看 AnnotatedStackTraceElement: AnnotatedStackTraceElement 源码[2]
*
* A class encapsulating a StackTraceElement and lock state. This adds
* critical thread state to the standard stack trace information, which
* can be used to detect deadlocks at the Java level.
*
* @hide
*/
@SystemApi(client = MODULE_LIBRARIES)
public final class AnnotatedStackTraceElement {
private StackTraceElement stackTraceElement;
private Object[] heldLocks;
private Object blockedOn;
}
隐藏类 AnnotatedStackTraceElement
接口详细定义如上图所示,系统定义这个类的最初目的也是为了做死锁检测。
其中:成员变量 heldLocks
为线程持有锁对象数组,成员变量 blockedOn
为线程等待锁对象。
通过反射手段,可以获取这些锁信息,然后基于这些锁信息就可以进行死锁检测。
死锁检测完整流程
线程死锁检测的整个过程详细描述如下:
死锁检测模块获取到所有线程对象之后,以线程对象为参数,通过反射机制调用 VMStack#getAnnotatedThreadStackTrace()
接口。会得到线程的AnnotatedStackTraceElement
数组。死锁检测模块在拿到所有线程的 AnnotatedStackTraceElement
数组之后,死锁检测模块将其封装成 Node 集合,Node 里包含锁之间的依赖关系:持有锁对象依赖等待锁对象。死锁检测模块将 Node 集合给到有向无环图模块进行环路检测。 有向无环图模块会返回环路检测结果。死锁检测模块如果发现存在环路,则判断为存在死锁。
案例分享
子进程线程死锁导致主进程 ANR
{
"case1":{
"threadName":"thread-1",
"threadStackList":[
"com.alibaba.dingtalk.android.o.a(Unknown Source:???)",
"- waiting on <90707987> (a com.alibaba.dingtalk.android.o)",
"com.alibaba.dingtalk.android.q.a(SourceFile:???)",
"- locked <106576464> (a com.alibaba.dingtalk.android.v)",
"com.alibaba.dingtalk.android.v.a(SourceFile:???)",
"- locked <106576464> (a com.alibaba.dingtalk.android.v)",
"com.alibaba.dingtalk.android.xxx.hta(SourceFile:???)",
"com.alibaba.dingtalk.mp.service.psc$b$b.run(SourceFile:???)",
"android.os.Handler.handleCallback(Handler.java:900)",
"android.os.Handler.dispatchMessage(Handler.java:103)",
"android.os.Looper.loop(Looper.java:219)",
"android.os.HandlerThread.run(HandlerThread.java:67)"
]
},
"case2":{
"name":"thread-2",
"threadStackList":[
"com.alibaba.dingtalk.android.r.a(SourceFile:???)",
"- waiting on <106576464> (a com.alibaba.dingtalk.android.v)",
"com.alibaba.dingtalk.android.r.a(SourceFile:???)",
"com.alibaba.dingtalk.android.o.a(SourceFile:???)",
"- locked <90707987> (a com.alibaba.dingtalk.android.o)",
"com.alibaba.dingtalk.android.r.b(SourceFile:???)",
"com.alibaba.dingtalk.android.o$h.b(SourceFile:???)",
"com.alibaba.dingtalk.android.r0$b.b(SourceFile:???)",
"com.alibaba.dingtalk.android.d0$d.run(SourceFile:???)",
"android.os.Handler.handleCallback(Handler.java:900)",
"android.os.Handler.dispatchMessage(Handler.java:103)",
"android.os.Looper.loop(Looper.java:219)",
"android.os.HandlerThread.run(HandlerThread.java:67)"
]
}
}
ANRCanary 收集到死锁信息示例如上:
该案例属于非常经典的案例,我们从线上监控到主进程发生 ANR,从 ANR Trace来看,都是卡在跨进程通信。 由于子进程没有发生 ANR,所以缺乏子进程的 Trace 信息,无法定位到跨进程通信耗时的根本原因。 但是从 ANRCanary 的死锁监控日志中,发现子进程线程死锁的上报记录。 如上所示,两个线程进入循环等待的状态: 线程 thread-1 持有锁 ID:106576464, 等待着锁 ID:90707987 线程 thread-2 持有锁 ID:90707987,等待着锁 ID:106576464 在解决子进程线程死锁的问题之后,主进程的 ANR 问题也得到解决。
2.2 Barrier 消息泄露
Barrier 消息泄露是导致 nativePollOnce ANR 的原因之一,同时一旦发生 Barrier 消息泄露,用户会连续 ANR ,非常容易引起客诉。
Android 的 Barrier 消息机制
Android 的 Barrier 消息是消息队列中的一类特殊消息,并不能被执行,是为了让主线程优先执行 UI 刷新类消息(也称为异步消息)而存在的。 Barrier 消息像一道栅栏,将消息队列里的普通消息先拦住,等最后一个 UI 刷新类消息(也称为异步消息)执行完以后,撤掉栅栏,普通消息(包括会导致 ANR 的消息)才得以继续执行。 如上图所示,在 Barrier 消息的作用下,消息队列中的任务执行顺序变为:1,4,5,2,3,6 。
如果最后一个异步消息丢失了(没有进队列或进了队列被误删),栅栏没有撤掉,就发生了 Barrier 消息泄露,那普通消息将会永远被拦住得不到执行,主线程将永远只会执行异步消息,从而导致 ANR 。 Barrier 机制是系统内部机制,通常不会有问题,但是可能有一些错误的业务场景会导致这类问题,比如子线程操作 UI,引发线程安全问题,会概率性导致 Barrier 消息泄露。
Barrier 消息泄露检测机制
ANRCanary 的消息泄露检测机制具体实现如下:
独立子线程定时触发,当主线程中消息队列的第一个消息为 Barrier 消息,且该 Barrier 消息阻塞超过 10 秒,认定为疑似 Barrier 泄露,启动校验机制。 校验机制会往主线程依次分别发送三个异步消息,三个同步消息,共 6 个消息。 其中异步消息会对一个校验值 +1, 同步消息会将校验值赋 0 。 如果前面的 Barrier 消息没有发生泄露,则异步消息和同步消息会依次执行,校验值最终为 0 。 如果 Barrier 消息发生了泄露,则只有异步消息会执行,校验值最终会变为 3 。 当校验值变为 3 ,则可以认定为发生了 Barrier 泄露,检测机制会执行该 Barrier 消息的移除,自动修复该异常。
3. 聚合签名
如果要建一份基于 ANR 归因的大盘报表,需要从用户每次 ANR 信息中提取出一个 KEY 字符串,称之为聚合签名,对于聚合签名的规则要求基本如下:
不同的 ANR 原因,聚合签名不相同 不同用户,相同的 ANR 原因,聚合签名相同 不同 App 版本,相同的 ANR 原因,聚合签名相同 聚合签名数量不能无限扩张
下面以一个最复杂的 Huge 类型的 Android Message 任务为例,说明一下聚合签名的组成部分。
huge|Choreographer$FrameHandler|Choreographer$FrameDisplayEventReceiver|0|andorid.widget.ListView.makeAndAddView
一个 Android Message 任务的聚合签名由三部分组成:
归因类型:导致 ANR 的主要原因类型。 消息信息:具体包含:Handler 类信息,Runnable 类信息,what 信息。 关键函数信息:依据提取出来的关键函数信息可以进一步拆分,将同一个函数导致的 ANR 问题,聚合在一起。
4. ANR 归因计算
ANRCanary 信息的生成时机是在用户发生 ANR 时,这时拿到的第一手资料包括:历史任务,当前 Running 任务,Pending 消息列表相关的各种信息。
ANR 归因计算的目标就是基于这第一手资料,将导致 ANR 的原因确定下来。
5. 关键函数提取
一个主线程任务(比如 Activity 启动等)执行涉及的代码可能会非常多,不同用户导致消息执行耗时的原因可能也不尽相同,需要基于堆栈比较,得出最耗时函数,称之为关键函数。
5.1 样例说明
为了方便说明,先将问题简化为假设所有的堆栈深度为 10 ,且堆栈之间的采样间隔是一样的。
样例1: 假设一个任务里面有 5 份堆栈,前两个堆栈相同的深度是 8,后三个堆栈相同的深度也是 8 。 相同的是深度,后三个堆栈的耗时更长。 则最耗时的函数为后三个堆栈里深度为 8 的那个函数。
样例2: 假设一个任务里面有 4 份堆栈,前两个堆栈相同的深度是 5,后两个堆栈相同的深度是 8 。 相同的是耗时,后两个堆栈的深度更深。 越深的函数,和业务代码的关联度更高,则取后两个堆栈里深度为 8 的那个函数。
样例3: 假设一个任务里面有4份堆栈,4 份堆栈的相同深度为5,中间两个堆栈相同的深度为 8 。 左边函数更耗时,右边的函数更深,此时应该如何取舍呢?
5.2 归一化权值计算
将耗时(duration)和栈深度(deep)两个数据,分别作为 X 轴和 Y 轴。 以样例 3 为例,最大耗时值为 4 (因为是 4 份堆栈),最大深度为 10 。 则左边的函数,耗时为 4 ,归一化之后为 1.0 ;深度为 5 ,归一化之后为 0.5 。 则右边的函数,耗时为 2 ,归一化之后为 0.5 ;深度为 8 ,归一化之后为 0.8 。 分别计算左边函数和右边函数到原点的距离,由此得出左边函数比右边函数权值更大,因此应该取左边的函数。
5.3 无关键函数
聚合签名中没有关键函数的情况,主要有两种:
如果该 Huge 任务,只有一个或没有堆栈,无法进行堆栈比较,自然就没有关键函数。 如果关键函数就是消息执行的根函数,比如:Handler#handleMessage 或 Runnable#run ,也应该当做没有关键函数处理。
6. ANR 归因监控平台
基于聚合签名进行聚合计数并排序,可以得出 App 中头部 ANR 问题的排名。由于该排名方式和Crash SDK 的 ANR Trace 堆栈聚合的排名方式不相同,因此排名第一的将不再是 nativePollOnce。
7. 后续
有了 ANR 归因监控平台之后,就可以从头部问题开始治理钉钉的 ANR 问题。
接下来下篇文章介绍 ANRCanary 在钉钉的 ANR 治理实战中遇到的各种案例 Case,看看实际效果如何。
参考资料
VMStack 源码: https://cs.android.com/android/platform/superproject/+/master:libcore/libart/src/main/java/dalvik/system/VMStack.java;l=35?q=VMStack&sq=&ss=android%2Fplatform%2Fsuperproject
[2]AnnotatedStackTraceElement 源码: https://cs.android.com/android/platform/superproject/+/master:libcore/libart/src/main/java/dalvik/system/AnnotatedStackTraceElement.java