ANR优化实践

Posted FullStackDeveloper

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ANR优化实践相关的知识,希望对你有一定的参考价值。

博客结构


用简单通俗易懂的话和可执行方式来学习ANR

1.为什么设计ANR机制

从用户角度,当界面/事件处于长时间阻塞,给用户弹框以提示,提高用户良好体验感。
从开发者角度看,用于监控应用的性能,并进行性能异常提示。
android系统设计的一套监控界面线程处于流畅

2.ANR是什么

2.1 定义

ANR全称Applicatipon No Response,应用无响应。

2.2 表象

页面长时间无响应时, 系统层面会弹出上图那样的ANR对话框。

2.3 本质

ANR就是应用的主线程处于阻塞状态过长,Android系统设计的一种监控机制。
Android 设计 ANR 的用意,是系统通过与之交互的组件(Activity,Service,Receiver,Provider)以及用户交互(InputEvent)进行超时监控,以判断应用进程(主线程)是否存在卡死或响应过慢的问题,通俗来说就是很多系统中看门狗(watchdog)的设计思想。

2.4 系统如何设计ANR机制

ANR机制实际上是超时监控机制,如何实现的监控机制?以Broadcast的超时监控,来总结分析实现。

简而言之,就是对于监控的组件/事件触发前,启动监控时间记录和超时Duration计算,从而到时间后检查是否未收到执行完消息,从而决定是否触发ANR的弹框。
1.计时机制是否会存在偏差,可以看handler.postDelay的实现?
2.其他组件/事件是否使用上面相同的原理?
3.计时是如何实现的?

2.5 ANR分类


1.什么是无感知场景?如何实现无感知场景判断?(无感知是否是在应用进程在前台?)
2.组件切换时设置TimeOut是什么意思?
从新计算超时TimeOut!

2.6 组件/事件ANR源码

2.6.1 Activity ANR

2.6.2 Boradcast ANR

2.6.3 Service ANR

2.6.4 ContentProvider AN

2.6.5 Input ANR

3.开发中如何避免ANR产生

(1)避免应用在主线程上非常缓慢地执行涉及 I/O 的操作。
(2)避免应用在主线程上进行长时间的计算。
(3)避免主线程在对另一个进程进行同步 binder 调用,而后者需要很长时间才能返回。
(4)避免主线程处于阻塞状态,为发生在另一个线程上的长操作等待同步的块。
(5)避免主线程在进程中或通过 binder 调用与另一个线程之间发生死锁。主线程不只是在等待长操作执行完毕,而且处于死锁状态。
(6)应用使用 Context.startForegroundService() 在前台启动新服务,但该服务在 5 秒内未调用 startForeground()

4.如何解决ANR问题

4.1 拉取trace文件

Android 会在遇到 ANR 时存储跟踪信息。在较低的操作系统版本中,设备上只有一个 /data/anr/traces.txt 文件。在较新的操作系统版本中,有多个 /data/anr/anr_* 文件。该操作需要超级权限,可以查看,不可以 adb pull

adb shell ls /data/anr
adb pull /data/anr/<filename>

4.1.1 错误上报获取方式

会在下面命令所在位置生成问题日志,其中就包括anr的trace.txt文件

adb bugreport


在指定的目录下查找anr文件。
## 4.1.2 Bugly获取方式

4.2 trace文件解析

常见文件格式如下:

Subject: executing service com.tencent.mobileqq/com.qzone.preview.service.PictureService

----- pid 30377 at 2023-03-25 09:56:07.689002960+0800 -----
Cmd line: com.tencent.mobileqq:qzone
Build fingerprint: 'OPPO/PDSM00/OP4EA3:12/SP1A.210812.016/R.c9327f_622e:user/release-keys'
ABI: 'arm64'
Build type: optimized
Zygote loaded classes=40698 post zygote classes=5119
Dumping registered class loaders
#0 dalvik.system.PathClassLoader: [], parent #1
#1 java.lang.BootClassLoader: [], no parent
#2 dalvik.system.PathClassLoader: [/data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/base.apk!classes9.dex:/data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/base.apk!classes162.dex:/data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/base.apk!classes3.dex:/data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/base.apk!classes17.dex:/data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/base.apk!classes27.dex:/data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/base.apk!classes7.dex:/data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/base.apk!classes22.dex:/data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/base.apk!classes18.dex:/data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/base.apk!classes10.dex:/data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/base.apk!classes15.dex:/data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/base.apk!classes23.dex:/data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/base.apk!classes21.dex:/data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/base.apk!classes57.dex:/data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/base.apk!classes12.dex:/data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/base.apk!classes14.dex:/data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/base.apk!classes2.dex:/data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/base.apk!classes165.dex:/data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/base.apk!classes19.dex:/data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/base.apk!classes163.dex:/data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/base.apk!classes167.dex:/data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/base.apk!classes11.dex:/data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/base.apk!classes164.dex:/data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/base.apk!classes24.dex:/data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/base.apk!classes26.dex:/data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/base.apk!classes20.dex:/data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/base.apk!classes105.dex:/data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/base.apk!classes5.dex:/data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/base.apk!classes25.dex:/data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/base.apk!classes16.dex:/data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/base.apk!classes8.dex:/data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/base.apk!classes28.dex:/data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/base.apk!classes166.dex:/data/user/0/com.tencent.mobileqq/files/dynamic_feature/kandian_feature_impl-debug.apk:/data/user/0/com.tencent.mobileqq/files/dynamic_feature/aelight_impl-debug.apk:/data/user/0/com.tencent.mobileqq/files/dynamic_feature/qqpay_temp_impl-debug.apk:/data/user/0/com.tencent.mobileqq/files/dynamic_feature/qqmini_impl-debug.apk:/data/user/0/com.tencent.mobileqq/files/dynamic_feature/qqrtc_feature_impl-debug.apk:/data/user/0/com.tencent.mobileqq/files/dynamic_feature/qq_comic_impl-debug.apk:/data/user/0/com.tencent.mobileqq/files/dynamic_feature/cmshow_impl-debug.apk:/data/user/0/com.tencent.mobileqq/files/dynamic_feature/zplan_impl-debug.apk:/data/user/0/com.tencent.mobileqq/files/dynamic_feature/webview_impl-debug.apk:/data/user/0/com.tencent.mobileqq/files/dynamic_feature/qqavgame_impl-debug.apk:/data/user/0/com.tencent.mobileqq/files/dynamic_feature/qqpubaccount_impl-debug.apk:/data/user/0/com.tencent.mobileqq/files/dynamic_feature/qqnearby_impl-debug.apk:/data/user/0/com.tencent.mobileqq/files/dynamic_feature/qqweishi_impl-debug.apk:/data/user/0/com.tencent.mobileqq/files/dynamic_feature/qqexpand_impl-debug.apk:/data/user/0/com.tencent.mobileqq/files/dynamic_feature/wxmini_impl-debug.apk:/data/user/0/com.tencent.mobileqq/files/dynamic_feature/profilecard_feature_impl-debug.apk:/data/user/0/com.tencent.mobileqq/files/dynamic_feature/party_room_impl-debug.apk:/data/user/0/com.tencent.mobileqq/files/dynamic_feature/relation_common_business_impl-debug.apk:/data/user/0/com.tencent.mobileqq/files/dynamic_feature/matchfriend_impl-debug.apk:/data/user/0/com.tencent.mobileqq/files/dynamic_feature/mutualmark_impl-debug.apk:/data/user/0/com.tencent.mobileqq/files/dynamic_feature/qq_winkpublish_impl-debug.apk:/data/user/0/com.tencent.mobileqq/files/dynamic_feature/qqecommerce_impl-debug.apk:/data/user/0/com.tencent.mobileqq/files/dynamic_feature/qqecommerce_shell_impl-debug.apk!classes2.dex:/data/user/0/com.tencent.mobileqq/files/dynamic_feature/qzone_df_impl-debug.apk!classes2.dex:/data/user/0/com.tencent.mobileqq/files/dynamic_feature/qqguild_df_impl-debug.apk:/data/user/0/com.tencent.mobileqq/files/dynamic_feature/qq_debug_impl-debug.apk:/data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/base.apk!classes13.dex:/data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/base.apk!classes90.dex:/data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/base.apk!classes4.dex:/data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/base.apk!classes156.dex:/data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/base.apk!classes67.dex:/data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/base.apk!classes132.dex:/data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/base.apk!classes92.dex:/data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/base.apk!classes51.dex:/data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/base.apk!classes33.dex:/data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/base.apk!classes65.dex], parent #1
Done dumping class loaders
Classes initialized: 0 in 0
Intern table: 50246 strong; 1493 weak
JNI: CheckJNI is on; globals=436 (plus 136 weak)
Libraries: /data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/lib/arm64/libc++_shared.so /data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/lib/arm64/libmmkv.so /data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/lib/arm64/libnative-memory-library-lib.so /data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/lib/arm64/libqimei.so /data/app/~~gyVwAxJwSrU-Un1LOEU9oQ==/com.tencent.mobileqq-V3MD34GlYOCA3DFJ55lYsA==/lib/arm64/libunitedconfig.so libandroid.so libaudioeffect_jni.so libcompiler_rt.so libframework-connectivity-jni.so libicu_jni.so libjavacore.so libjavacrypto.so libjnigraphics.so libmedia_jni.so libonetrace_jni.so libopenjdk.so liboplusextzawgyi.so liboplusgui_jni.so liboplushwui_jni.so libphoenix_jni术语表 

系统 ANR 完整流程

系统 ANR 完整流程可以分为如下三个部分:

  • 超时检测
  • ANR 信息收集
  • ANR 信息输出

对于超时检测的逻辑,业界已经有比较详细的阐述,此处不再赘述。重点聊聊检测到超时之后的处理逻辑。详细源码可以参见:ProcessRecord.java,ANR 信息收集和 ANR 信息输出两个流程图如下:

如上图所示,从系统源码中,得到的启示有:

  • ANR Trace 的堆栈抓取时机是滞后的,其堆栈不一定是 ANR 根因。
  • System Server 会对多个进程发送 SIGQUIT 信号,请求堆栈抓取的操作。
    • 收到 SIGQUIT 不代表当前进程发生了 ANR ,可能是手机里有一个其他的 App 发生了 ANR,如果不进行 ANR 的二次确认,就会导致 ANR 误报。
  • App 可以通过进程 ANR 错误状态感知发生了前台 ANR 。

刻舟求剑的 ANR Trace

  • 以广播发送导致 ANR 的过程为例,当 System Server 进程检测到广播处理超时时,会发送SIGQUIT 信号到 App 进程, App 进程收到信号之后,会将当前所有线程的执行堆栈 Dump 下来为 ANR Trace,并最终输出。
  • 然而如图所示,这个 Dump 时机是有一定的滞后性的,真正导致 ANR 的 长耗时消息3 已经执行完了。当前执行消息5 是作为替罪羊被抓到的,甚至 当前执行消息5 到底消耗了多长时间也不确定。因此 Android 系统设计提供的用来分析 ANR 问题的 ANR Trace ,其实只是刻舟求剑, 并不一定能定位到 ANR 的根因。

ANR 误报过滤

鉴于前面提到的收到 SIGQUIT 信号,并不代表当前进程发生了 ANR,需要一个二次确认逻辑,进行误报过滤。

钉钉采用的方案是:

  • 在收到 SIGQUIT 信号之后,在 20 秒内轮询进程错误状态的确认是否为前台 ANR。
  • 与此同时,因为发生后台 ANR 之后,系统会直接杀进程,而其他进程 ANR 并不会导致进程被杀,因此可以通过持久化的方案来区分。

详细流程图如下:

ANR 监控工具

工欲善其事,必先利其器。钉钉自研的 ANRCanary 监控工具,通过轮询的方式持续记录主线程最新任务的执行耗时,到发生 ANR 时,基于耗时最长的消息定位 ANR 的根因。

ANRCanary 相对于 ANR Trace,从点扩展到面,提供了主线程历史任务耗时维度的信息,解决了 ANR Trace 刻舟求剑的问题。

接下来将对上图中的关键技术方案依次进行详细说明。

历史任务监控

Android 主线程任务,可以大概划分为如下几个分类:

  • Handler 消息:最常见的基于 Handler 的主线程任务。
  • IdleHandler:消息队列进入空闲状态时执行。
  • nativePollOnce:从 Native 层触发,具体可能包括:
    • 触摸事件处理
    • 传感器事件处理

历史任务监控的目标是感知每个主线程任务的开始时间和结束时间,针对不同的主线程任务,需要采用不同的 Hook 方式。大部分的 Hook 方案,业界均有比较详细的描述,不再说明。

简单介绍一下 FakeIdle 排除法方案:

  • 基于定时的堆栈抓取能力,将堆栈始终处于 nativePollOnce 的任务,判断为 Idle 任务。
  • 那么既不是 Message 任务,也不是 IdleHandler 任务, 还不是 Idle 时间段其余任务,单独识别为 FakeIdle 任务。

历史任务聚合

对于 ANR 来说,需要重点关注的是长耗时的任务,大部分的短耗时任务是可以忽略的。因此任务调度是可以按照一定条件进行聚合。

任务聚合的好处具体包括:

  • 减少内存操作次数:避免内存抖动和对应用性能产生影响
  • 压缩冗余数据:方便观察和分析

基于上述思路,将聚合以后的主线程历史任务记录分成如下几个类型:

  • 聚合类型:主线程连续调度多个任务,并且每一个任务耗时都很少的情况下,将这些任务耗时累加。直至这些任务累计耗时超过阈值,则汇总并记录一条聚合类型的任务记录。该类型任务通常不需要关注。
  • Huge 类型:单个任务耗时超过设定的阈值,则单独记录一条 Huge类型的任务。同时将 Huge 任务前面尚未聚合的 N 次短时间耗时任务生成一条聚合类型的任务。该类型任务需要重点关注。
  • Idle 类型:主线程进入空闲状态的时间段,自然也应该生成一条记录。该类型任务通常不需要关注。
  • Key 类型:可能会引起 ANR 的Android 四大组件的消息,需要单独记录。称之为 Key 类型的记录。
  • Freeze 类型:部分厂商手机独有的 App 退后台,进程运行被冻结,直到 App 回到前台才会继续运行,被冻结的任务时间间隔可能会很长,却不能当做 Huge 类型,需要单独记录为 Freeze 类型。

当前 Running 任务

通过 ANRCanary 的当前 Running 任务信息,可以清晰的知道当前任务到底执行了多长时间,帮忙研发人员排除干扰,快速定位。 借助这项监控,可以非常直观的看到 ANR 的 Trace 堆栈刻舟求剑的问题。

"runningTaskInfo":
    "stackTrace":[
        "android.os.MessageQueue.nativePollOnce(Native Method)",
        "android.os.MessageQueue.next(MessageQueue.java:363)",
        "android.os.Looper.loop(Looper.java:176)",
        "android.app.ActivityThread.main(ActivityThread.java:8668)",
        "java.lang.reflect.Method.invoke(Native Method)",
        "com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513)",
        "com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1109)"
    ],
    "type":"IDLE",
    "wallDuration":519

如上所示,基于 ANRCanary 抓取到信息,可以看到发生 ANR 时主线程当前处于 IDLE 状态,持续时间为 519 毫秒。

Pending消息列表

主线程的消息列表也是 ANRCanary 需要 Dump 的,基于 Pending 消息列表,可以感知以下几点:

  • 消息队列中的消息是否被 Block 以及被 Block 了多久:基于 Block 时长可以判断主线程的繁忙程度。
  • 判断是否存在 Barrier 消息泄露。一旦发生 Barrier 消息泄露,主线程会永久阻塞,导致永远处于 ANR 状态。
    • 关于 Barrier 消息泄露的问题,将在后续章节进行详细探讨。
  • 判断消息列表里是否存在重复消息:据此推断是否有业务逻辑异常导致重复任务,从而填满了主线程导致 ANR。

总的来说,如上图所示,ANRCanary 收集的主线程信息包括过去,现在,未来三个阶段。

主线程堆栈采样

每个主线程任务内部的业务逻辑对于研发人员来说都是黑盒。函数执行的耗时存在很多的不确定性。有可能是锁等待,跨进程通信,IO操作等各种情况都会导致任务执行耗时,因此需要堆栈信息帮忙定位到具体代码。

ANRCanary 实现的时间对齐的堆栈采样方案,主要目的包括:

  • 避免频繁添加、取消超时任务
  • 只有长耗时执行任务才会触发堆栈抓取
  • 尽可能减少堆栈抓取的次数

如上图所示,堆栈采样的时间对齐方案具体实现如下:

  • 由单独的堆栈采样线程负责堆栈抓取。
  • 基于主线程的任务监听机制,每个任务的开始和结束都会告知到堆栈采样线程。
  • 超时任务触发堆栈抓取的前提条件是:当前最新的主线程任务执行超过最低超时时间。
  • 执行完堆栈抓取后,会对超时时长进行渐进,再丢一个超时任务,直到当前任务执行完成
  • 长耗时后面的任务发现超时时长发生过渐进,就会执行一次堆栈采样线程消息队列的清理,重置超时任务。

案例分享

我们收到一例测试同学的反馈,说钉钉在长时间压测过程中,总是遇到 ANR 问题,阻塞了测试流程。

基于 BugReport 里的 ANR Trace 信息显示,是传感器事件处理耗时的问题。

"main" prio=5 tid=1 Runnable
  | group="main" sCount=0 dsCount=0 flags=0 obj=0x749019e8 self=0x743c014c00
  | sysTid=26378 nice=-10 cgrp=default sched=0/0 handle=0x74c1b47548
  | state=R schedstat=( 12722053200 4296751760 139559 ) utm=949 stm=323 core=2 HZ=100
  | stack=0x7febba5000-0x7febba7000 stackSize=8MB
  | held mutexes= "mutator lock"(shared held)
  at java.io.CharArrayWriter.<init>(CharArrayWriter.java:67)
  at java.io.CharArrayWriter.<init>(CharArrayWriter.java:58)
  at java.net.URLEncoder.encode(URLEncoder.java:206)
  at xxx.b(SourceFile:???)
  at xxx.build(SourceFile:???)
  at xxx.onEvent(SourceFile:???)
  at xxx.onEvent(SourceFile:???)
  at xxx.handleSensorEvent(SourceFile:???)
  - locked <0x00052b82> (a sco)
  at android.hardware.SystemSensorManager$SensorEventQueue.dispatchSensorEvent(SystemSensorManager.java:833)
  at android.os.MessageQueue.nativePollOnce(Native method)
  at android.os.MessageQueue.next(MessageQueue.java:326)
  at android.os.Looper.loop(Looper.java:160)
  at android.app.ActivityThread.main(ActivityThread.java:6718)
  at java.lang.reflect.Method.invoke(Native method)
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)

可是基于 钉钉接入的 CrashSDK 里的 ANR Trace 信息显示,是硬件渲染的问题。

"main" prio=10 tid=1 Native
  | group="" sCount=0 dsCount=0 flags=0 obj=0x749019e8 self=0x7602814c00
  | sysTid=25052 nice=-10 cgrp=default sched=0/0 handle=0x7688258548
  | state=? schedstat=( 0 0 0 ) utm=0 stm=0 core=0 HZ=100
  | stack=0x7fe795e000-0x7fe7960000 stackSize=8MB
  | held mutexes=
  at android.view.ThreadedRenderer.nSyncAndDrawFrame(Native method)
  at android.view.ThreadedRenderer.draw(ThreadedRenderer.java:823)
  at android.view.ViewRootImpl.draw(ViewRootImpl.java:3321)
  at android.view.ViewRootImpl.performDraw(ViewRootImpl.java:3125)
  at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2484)
  at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1466)
  at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:7196)
  at android.view.Choreographer$CallbackRecord.run(Choreographer.java:949)
  at android.view.Choreographer.doCallbacks(Choreographer.java:761)
  at android.view.Choreographer.doFrame(Choreographer.java:696)
  at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:935)
  at android.os.Handler.handleCallback(Handler.java:873)
  at android.os.Handler.dispatchMessage(Handler.java:99)
  at android.os.Looper.loop(Looper.java:193)
  at android.app.ActivityThread.main(ActivityThread.java:6718)
  at java.lang.reflect.Method.invoke(Native method)
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)

两个结论僵持不下,最后再来看看 ANRCanary 提供的信息

  "cpuDuration": 9,
    "messageStr": ">>>>> Dispatching to Handler (android.view.Choreographer$FrameHandler) 3b01fdc android.view.Choreographer$FrameDisplayEventReceiver@bdac8e5: 0",
    "threadStackList": [
        ...
        
            "stackTrace":[
                "android.view.ThreadedRenderer.nSyncAndDrawFrame(Native Method)",
                "android.view.ThreadedRenderer.draw(ThreadedRenderer.java:823)",
                "android.view.ViewRootImpl.draw(ViewRootImpl.java:3321)",
                "android.view.ViewRootImpl.performDraw(ViewRootImpl.java:3125)",
                "android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2484)",
                "android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1466)",
                "android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:7196)",
                "android.view.Choreographer$CallbackRecord.run(Choreographer.java:949)",
                "android.view.Choreographer.doCallbacks(Choreographer.java:761)",
                "android.view.Choreographer.doFrame(Choreographer.java:696)",
                "android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:935)",
                "android.os.Handler.handleCallback(Handler.java:873)",
                "android.os.Handler.dispatchMessage(Handler.java:99)",
                "android.os.Looper.loop(Looper.java:193)",
                "android.app.ActivityThread.main(ActivityThread.java:6718)",
                "java.lang.reflect.Method.invoke(Native Method)",
                "com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)",
                "com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)"
            ],
            "state":"RUNNABLE",
            "wallTime":65347
        
    ],
    "type": "HUGE",
    "wallDuration": 68497

ANRCanary的信息显示钉钉在硬件渲染阶段耗费了 68 秒的时间。


    "curThreadStack":
        "stackTrace":[
            "android.os.MessageQueue.enqueueMessage(MessageQueue.java:569)",
            "- locked <192655128> (a android.os.MessageQueue)",
            "android.os.Handler.enqueueMessage(Handler.java:745)",
            "android.os.Handler.sendMessageAtTime(Handler.java:697)",
            "android.os.Handler.postAtTime(Handler.java:445)",
            "xxx.send(SourceFile:???)",
            "xxx.handleSensorEvent(SourceFile:???)",
            "- locked <189021104> (a xxx)",
            "android.hardware.SystemSensorManager$SensorEventQueue.dispatchSensorEvent(SystemSensorManager.java:833)",
            "android.os.MessageQueue.nativePollOnce(Native Method)",
            "android.os.MessageQueue.next(MessageQueue.java:326)",
            "android.os.Looper.loop(Looper.java:160)",
            "android.app.ActivityThread.main(ActivityThread.java:6718)",
            "java.lang.reflect.Method.invoke(Native Method)",
            "com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)",
            "com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)"
        ],
        "state":"RUNNABLE",
        "wallTime":12
    ,
    "messageStr": "",
    "type": "LOOPER",
    "wallDuration": 12

而一开始 BugReport 指出的传感器事件处理,作为当前 Running 任务,只耗费了 12 毫秒。

最终基于 ANRCanary 给出的排查方向,开发同学定位到阻塞的原因是因为测试机系统硬件渲染底层有一个锁等待导致的问题。

后续

本篇文章介绍了钉钉自研的 ANRCanary 通过监控主线程的执行情况,为定位 ANR 问题提供更加丰富的信息。不过 ANRCanary 日志信息比较多,希望每个研发人员都能从中分析出导致 ANR 的原因是比较困难的。

接下来将在下篇文章中讲述钉钉基于分析算法得出 ANR 归因,并上报到 ANR 归因监控平台,帮助研发人员更快更准确的解决 ANR 问题。

作者:阿里巴巴终端技术
链接:https://juejin.cn/post/7181731795439157306

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。


相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

全套视频资料:

一、面试合集

二、源码解析合集


三、开源框架合集


欢迎大家一键三连支持,若需要文中资料,直接点击文末CSDN官方认证微信卡片免费领取↓↓↓

以上是关于ANR优化实践的主要内容,如果未能解决你的问题,请参考以下文章

抖音 Android 性能优化系列:Java 内存优化篇

Android ANR优化 1

Android App优化之ANR详解

Android性能优化--ANR

Android ANR优化 2

Android 进阶——性能优化之因Handler引起句柄泄漏导致ANR的定位和解决