android anr 监控方案与分析

ANR产生机制

Anr,Applicatipon No Response,如果App在特定时间无法相应屏幕触摸或键盘输入时间,或者特定事件没有处理完毕,就会出现ANR。分类:

  • InputDispatching:5秒内无响应input事件,下一个事件到达发现有一个超时事件才会触发ANR。关键字:Input event dispatching timed out
  • BroadcastQueue:前台广播的onReceive()函数时10秒没有处理完成,后台为60秒。关键字:Timeout of broadcast BroadcastRecord
  • Service:前台20秒,后台200秒内没有执行完毕,前后台是根据app的状态来判定的。关键字:Timeout executing service
  • ContentProvider:创建发布超时并不会ANR,可以自己设定anr。关键字:timeout publishing content providers

产生anr时不一定会出现弹窗提示。

Anr 监控与日志收集

anr系统日志获取

发生anr后,系统会收集不同纬度的信息来生成anr trace文件。Android 6.0 后,应用侧无法访问data/anr/trace 文件,那app层如何得知anr与获取日志哪?

主线程 watchdog 机制

类似Blockcandary实现机制,检测主线程Message是否在指定时间得到处理。如果规定时间内没有处理就认为发生了ANR。弊端:监测的条件也不一定准确。

监听 SIGNALQUIT 信号

系统的 system_server 进程在检测到 App 出现 ANR 后,会向出现 ANR 的进程发送 SIGQUIT (signal 3) 信号。正常情况下,系统的 libart.so 会收到该信号,并调用 Java 虚拟机的 dump 方法生成 traces。这也是apm常用的方法。

app 可以 拦截 SIGQUIT,来生成 traces 和 ANR 日志。在 app 处理完信号后,会将信号继续传递给系统的 libart.so,让系统生成 ANR traces.txt。

日志分析时的影响因素

经常发现 ANR in xxx 处的代码并不会造成 anr,为什么还会报在此处哪?

anr的监测并不是真的监控发送的消息在真实执行过程中是否超时,线程dump时不管当前消息有没有被执行,或者真实执行过程耗时有多久,只要在监控超时到来之前,服务端没有接收到通知,那么就判定为超时。

总结来说,发生 ANR 问题时,Trace 堆栈很多情况下都不是 RootCase。而系统 ANR Info 中提示某个 Service 或 Receiver 导致的 ANR 在很大程度上,并不是这些组件自身问题。如下例子:

之前某个历史消息严重耗时,但是直到该消息执行完毕,系统服务仍然没有达到触发超时的临界点,后续主线程继续调度其它消息时,系统判定响应超时,那么正在执行的业务场景很不幸被命中,即使当前正在执行的业务逻辑可能很简单。这种场景在线上大量存在,比较隐蔽。发生 ANR 时主线程消息调度示意图如下:

****

分析这类问题是需要apm具有消息调度监控的功能,才能方便排查问题,只根据系统产生的一些日志是很难分析出具体原因的。

消息调度监控

ANR 很多场景都是历史消息耗时较长并不断累加后导致的,如果可以监控每次消息调度的耗时并记录,是不是就可以清晰的知道 ANR 发生前主线程都发生了什么?

耗时消息堆栈采样:

  • 方案1:是对每个函数进行插桩,在进入和退出过程统计其耗时,并进行聚合和汇总。该方案的优点是可以精确的知道每个函数的真实耗时,缺点是很影响包体积和性能,而且不利于其他产品高效复用。
  • 方案2,在每个消息开始执行时,触发子线程的超时监控,如果在超时之后本次消息还没执行结束,则抓取主线程堆栈。 参考 BlockCandary 实现,通过Looper Printer可以拿到每次信息的处理情况。大部分消息耗时都很少,如果每次都频繁设置和取消将会带来性能影响,因此一定要对此进行优化。

待调度消息采样

同时除了监控主线程历史消息调度及耗时之外,也需要在 ANR 发生时,获取消息队列中待调度的消息,为分析问题提供更多线索。可通过反射获取消息队列下一个待调度消息,然后通过Message.next字段遍历链表

接下来看一个实例,从下图可以看到主线程正在调度的消息耗时超过 1S,但是在此之前的另一个历史消息耗时长达 9828ms。继续观察下图消息队列待调度的消息状态(灰色示意),可以看到第一个待调度的消息被 Block 了 14S 之久。由此我们可以知道 ANR 消息之前的这个历史消息,才是导致 ANR 的罪魁祸首,当然这个正在执行的消息也需要优化一下性能。

如果没有消息调度监控工具,上去就盲目的分析当前逻辑调用的问题,可能就犯了方向性的错误,掉入"Trace 堆栈"陷阱中。

ANR 分析思路

日志主要包括以下几种:

  • Trace 日志
  • AnrInfo
  • Kernel 日志
  • Logcat 日志
  • Meminfo 日志
  • 其它监控工具

Trace 信息

系统会 Dump 当前进程以及关键进程的线程堆栈:

less 复制代码
suspend all histogram:  Sum: 2.834s 99% C.I. 5.738us-7145.919us Avg: 607.155us Max: 41543us
DALVIK THREADS (248):
"main" prio=5 tid=1 Native
  | group="main" sCount=1 dsCount=0 flags=1 obj=0x74b17080 self=0x7bb7a14c00
  | sysTid=2080 nice=-2 cgrp=default sched=0/0 handle=0x7c3e82b548
  | state=S schedstat=( 757205342094 583547320723 2145008 ) utm=52002 stm=23718 core=5 HZ=100
  | stack=0x7fdc995000-0x7fdc997000 stackSize=8MB
  | held mutexes=
  kernel: __switch_to+0xb0/0xbc
  kernel: SyS_epoll_wait+0x288/0x364
  kernel: SyS_epoll_pwait+0xb0/0x124
  kernel: cpu_switch_to+0x38c/0x2258
  native: #00 pc 000000000007cd8c  /system/lib64/libc.so (__epoll_pwait+8)
  native: #01 pc 0000000000014d48  /system/lib64/libutils.so (android::Looper::pollInner(int)+148)
  native: #02 pc 0000000000014c18  /system/lib64/libutils.so (android::Looper::pollOnce(int, int*, int*, void**)+60)
  native: #03 pc 0000000000127474  /system/lib64/libandroid_runtime.so (android::android_os_MessageQueue_nativePollOnce(_JNIEnv*, _jobject*, long, int)+44)
  at android.os.MessageQueue.nativePollOnce(Native method)
  at android.os.MessageQueue.next(MessageQueue.java:330)
  at android.os.Looper.loop(Looper.java:169)
  at com.android.server.SystemServer.run(SystemServer.java:508)
  at com.android.server.SystemServer.main(SystemServer.java:340)
  at java.lang.reflect.Method.invoke(Native method)
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:536)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:856)

常用的关键信息进行说明,如下:

  • 线程堆栈:发生 ANR 时线程正在执行的逻辑,但是很多场景下,获取的堆栈耗时并不长。
  • 线程状态:"state=xxx"表示当前线程工作状态
    • Running 表示当前线程正在被 CPU 调度
    • Runnable 表示线程已经 Ready 等待 CPU 调度
    • Native 态则表示线程由 Java 环境进入到 Native 环境,可能在执行 Native 逻辑,也可能是进入等待状态
    • Waiting 表示处于空闲等待状态
    • 除此之外还有 Sleep,Blocked 状态等等
  • 线程耗时:"utmXXX,stmXXX",表示该线程从创建到现在,被 CPU 调度的真实运行时长,不包括线程等待或者 Sleep 耗时,其中线程 CPU 耗时又可以进一步分为用户空间耗时(utm)和系统空间耗时(stm),这里的单位是 jiffies,当 HZ=100 时,1utm 等于 10ms。
    • utm: Java 层和 Native 层非 Kernel 层系统调用的逻辑,执行时间都会被统计为用户空间耗时;
    • stm: 即系统空间耗时,一般调用 Kernel 层 API 过程中会进行空间切换,由用户空间切换到 Kernel 空间,在 Kernel 层执行的逻辑耗时会被统计为 stm,如文件操作,open,write,read 等等;
  • core:最后执行这个线程的 cpu 核的序号。

  • 线程优先级:nice 该值越低,代表当前线程优先级越高,理论上享受的 CPU 调度能力也越强。对于应用进程(线程)来说,nice 范围基本在 100~139。随着应用所在前后台不同场景,系统会对进程优先级进行调整

  • 调度态:"schedstat=( 1813617862 14167238 546 )",分别表示线程 CPU 执行时长(单位 ns),等待时长,Switch 次数。

Anr Info 信息

系统会在发生 ANR 时获取一些系统状态,如 ANR 问题发生之前和之后的系统负载以及 Top 进程和关键进程 CPU 占用比:

yaml 复制代码
11:26:19.996 1657-11869/? E/ActivityManager: ANR in com.***.***.***
    PID: 11093
    Reason: Input dispatching timed out (*****.**CheckInActivity_ (server) is not responding. Waited 8014ms for MotionEvent(action=DOWN))
    Load: 3.89 / 2.63 / 2.44
    CPU usage from 0ms to 6012ms later (2021-11-12 11:26:13.938 to 2021-11-12 11:26:19.949):
      53% 1657/system_server: 32% user + 21% kernel / faults: 14664 minor 589 major
      52% 11093/com.***.***.***: 37% user + 14% kernel / faults: 17832 minor 34 major
      19% 25179/cnss_diag: 11% user + 7.6% kernel / faults: 7 minor
      12% 853/surfaceflinger: 4.1% user + 8.4% kernel / faults: 503 minor 7 major
      10% 499/logd: 4.3% user + 6.4% kernel / faults: 22 minor
      0% 1227/media.swcodec: 0% user + 0% kernel / faults: 19869 minor 68 major
      10% 10580/com.miui.securitycenter: 5.8% user + 4.4% kernel / faults: 4022 minor 3 major
      9.2% 9835/adbd: 3.4% user + 5.8% kernel / faults: 14 minor
      7.9% 3307/com.android.systemui: 5.4% user + 2.4% kernel / faults: 1546 minor 2 major
      7.6% 23896/com.google.android.gms.persistent: 4.4% user + 3.1% kernel / faults: 3190 minor 29 major
      ...
      0.4% 90/system: 0% user + 0.4% kernel
      ........
66% TOTAL: 20% user + 15% kernel + 28% iowait + 0.7% irq + 0.7% softirq
  • ANR 类型: Reason 表示当前是哪种类型的消息或应用组件导致的 ANR,如 Input,Receiver,Service 等等。
  • 系统负载(Load):"Load:45.53 / 27.94 / 19.57",分布代表 ANR 发生前 1 分钟,前 5 分钟,前 15 分钟各个时间段系统 CPU 负载,具体数值代表单位时间等待系统调度的任务数(可以理解为线程)。如果这个数值过高,则表示当前系统中面临 CPU 或 IO 竞争,此时,普通进程或线程调度将会受到影响。
  • 进程 CPU 使用率:表示当前 ANR 问题发生之前(CPU usage from XXX to XXX ago)或发生之后(CPU usage from XXX to XXX later)一段时间内,CPU 占用,并列出这些进程的 user,kernel 的 CPU 占比。当然很多场景会出现 system_server 进程 CPU 占比较高的现象,针对这个进程需要视情况而定,至于 system_server 进程 CPU 占比为何普遍较高,ANR 时,系统进程除了发送信号给其它进程之外,自身也 Dump Trace,并获取系统整体及各进程 CPU 使用情况,且将其它进程 Dump 发送的数据写到文件中。因此这些开销将会导致系统进程在 ANR 过程承担很大的负载,这是为什么我们经常在 ANR Trace 中看到 SystemServer 进程 CPU 占比普遍较高的主要原因。单进程CPU的负载并不是以100%为上限,而是有几个核,就有百分之几百,如4核上限为400%
  • 关键进程:
    • kswapd: 是 linux 中用于页面回收的内核线程,主要用来维护可用内存与文件缓存的平衡,以追求性能最大化,当该线程 CPU 占用过高,说明系统可用内存紧张,或者内存碎片化严重,需要进行 file cache 回写或者内存交换(交换到磁盘),线程 CPU 过高则系统整体性能将会明显下降,进而影响所有应用调度。
    • mmcqd: 内核线程,主要作用是把上层的 IO 请求进行统一管理和转发到 Driver 层,当该线程 CPU 占用过高,说明系统存在大量文件读写,当然如果内存紧张也会触发文件回写和内存交换到磁盘,所以 kswapd 和 mmcqd 经常是同步出现的。
  • 系统 CPU 分布:"66% TOTAL: 20% user + 15% kernel + 28% iowait + 0.7% irq + 0.7% softirq"。系统整体 CPU 使用率,以及 user,kernel,iowait 方向的 CPU 占比,如果发生大量文件读写或内存紧张的场景,则 iowait 占比较高。

Logcat 日志

在 log 日志中,还有一些关键字也可以帮我们去推测当前系统性能是否遇到问题,"low memory""Slow operation","Slow delivery" 等等。也可以观察当前进程和系统进程是否存在频繁 GC 等等,以帮忙我们更全面的分析系统状态。

案例分析

历史消息耗时严重

发生 ANR 时,Trace 堆栈落在了 NativePollOnce 这是很常见的case。这个场景一般表示当前线程处于空闲或 JNI 执行完之后,进行上下文切换的过程。如果仅仅依靠系统日志让我们很难进一步分析。

主线程 Trace 堆栈:

看到上面的 Trace 堆栈,基本无从下手,线程状态以及线程 utm,stm 时长都没有明显异常,但是线上确实有大量的 ANR 问题落在这个场景。移步到系统层面,看看是否有线索可寻。

分析系统&进程负载:

观察系统负载: 在 ANR Info 中查看Load关键字,发现系统在前 1 分钟,前 5 分钟,前 15 分钟各个时段负载并不算高,但是最近 1 分钟 CPU 负载为 11.81,比前 5,15 分钟都高,说明负载有加重趋势。

观察进程 CPU 分布: 进一步观察 CPU usage from 0 ms to 5599 later 关键字,看到 ANR 之后这 5S 多的时间,我们应用主进程 CPU 占比达到 155%,而且在 user,kernel 层面 CPU 占比分布上,user 占比 124%,明显很高,这时直觉告诉我们,当前进程应该有问题。

观察系统 CPU 分布:

进一步分析系统负载,发现整体 CPU 使用率并不高。user 占比 17%,kernel 占比 7.5%,iowait 占比 0.7%,说明这段时间系统 IO 并不繁忙,整体也符合预期。

系统侧结论: 通过观察系统负载和各个进程的 CPU 使用情况,发现系统环境比较正常,但是我们的主进程 CPU 占比明显偏高,但因本次 Anr Info 中未获取到进程内部各线程 CPU 使用率,需要回到应用侧进一步分析。

应用侧分析:

根据上面的分析,我们将方向转回到当前进程,通过对比 Trace 中各线程耗时(utm+stm),发现除了主线程之外,其它部分子线程耗时(utm+stm)没有明显异常,因此基本排除了进程内部子线程 CPU 竞争导致的问题,因此问题进一步聚焦到主线程。

接下来再将目光聚焦在主线程消息调度情况,通过监控工具观察主线程的历史消息,当前消息以及待调度消息,如下图:

通过上图可以看到当前消息调度 Wall Duration 为 46ms。在ANR 之前存在一个索引为 46 的历史消息耗时严重(wall duration:10747ms) ,同时还有一个索引为 32 的历史消息耗时也比较严重(wall duration:10747ms)。

进一步观察消息队列第一个待调度消息,发现其实际 block 时长为 9487ms,因此我们排除了索引为 32 的历史消息,认为索引为 46 的消息耗时是导致后续消息未能及时调度的主要原因

在锁定索引为 46 的历史消息之后,接下来利用监控工具中的消息堆栈采样监控,发现该消息执行过程,多次堆栈采样均命中创建 Webview 逻辑。

NativePollOnce 场景耗时并不长,导致本次 ANR 的主要原因是历史消息在 UI 绘制过程中同步创建 Webview,导致耗时严重。但是期间系统超时监控并没有触发,待主线程继续调度后续消息时,系统监控客户端响应超时,捕获了主线程正在执行的逻辑

进程内 IO 负载异常

主线程 Trace 堆栈:

堆栈信息没有太明显,我们就把方向转移到系统侧,看看是否有线索。

系统&进程负载分析:

观察系统负载: 在 ANR Info 中我们搜索 Load 关键字,发现系统各个时段(前 1,5,15 分钟)负载明显很高,并且最近 1 分钟负载为 71,又明显高于前 5,15 分钟,说明有系统负载进一步加重!

观察进程 CPU 分布: 进一步观察 CPU usage from 0 ms to 21506 later 关键字,看到ANR 之后这段时间,内核线程 kworker 的 CPU 占比明显偏高,累计占比超过 45%!其它系统或应用进程 CPU 使用率普遍偏低。 通过前文介绍我们知道 kworker 属于内核线程,当 IO 负载过重时会在该线程有所体现。进一步观察 kswapd0 线程 cpu 使用率,发现只有 2%,而 kswapd0 主要应用在内存紧张的场景,说明这段时间系统内存负载基本正常。通过上面这些信息解读,可以大致推测最近一段时间系统负载过高,应该是 IO 请求导致,至于系统内存压力尚可接受,接下来我们继续求证。

观察进一步分析系统整体负载,发现 user 占比只有 5.4%,kernel 占比 11%,但是 iowait 占比高达 57%!明显高于 user,kernel 使用率,说明这段时间系统 IO 负载非常严重。

而这个 IO 占比较高,也进一步实锤了我们上面的"观察进程 CPU 分布"的结论。那么是哪个应用导致的呢?遗憾的,受限于系统日志获取能力,依靠现有的信息并没有明显看到异常进程,那么 IO 发生在哪里,是否和当前进程有关呢?于是我们将思路再次回到应用内部。

应用侧分析:

通过上面的分析,我们基本锁定了是 IO 负载过重导致的问题,接下来便要进一步排查是否是当前进程内部存在异常,于是我们对比了各个线程的耗时(utm+stm)情况:

通过上图线程耗时对比可以清晰的发现,DBHelper-AsyncOp-New 线程无论是 utm 时长,还是 stm 时长,都明显超过其它线程,而 stm 高达 2915! 这个耗时超出了我们的预期,实际场景中我们认为主线程才 CPU 消耗大户,其它线程都是配角。下面我们再看一下线程详情:

进一步查看该线程堆栈,发现存在明显的 IO 操作,而且子线程优先级(nice=0)相对较高,stm(2915)+utm(1259)高达 4000+,换算成时长相当于 CPU 真实执行超过了 40S!

对比主线程耗时(utm:1035,stm:216),以及进程启动时长(4 分 18 秒),可以更好证明了 DBHelper 线程存在异常,stm 明显过长,说明存在大量系统调用,结合该线程业务,可以很快就猜到是 IO 读写引起的问题了。因为该线程本身就是负责应用内部数据库清理功能的。

主线程被锁阻塞

ini 复制代码
"main" prio=5 tid=1 Blocked
  | group="main" sCount=1 dsCount=0 flags=1 obj=0x72deb848 self=0x7748c10800
  | sysTid=22838 nice=-10 cgrp=default sched=0/0 handle=0x77cfa75ed0
  | state=S schedstat=( 390366023 28399376 279 ) utm=34 stm=5 core=1 HZ=100
  | stack=0x7fce68b000-0x7fce68d000 stackSize=8192KB
  | held mutexes=
  at com.example.test.MainActivity$onCreate$1.onClick(MainActivity.kt:15)
  - waiting to lock <0x01aed1da> (a java.lang.Object) held by thread 3 ------------------关键行!!!
  at android.view.View.performClick(View.java:7187)
  at android.view.View.performClickInternal(View.java:7164)
  at android.view.View.access$3500(View.java:813)
  at android.view.View$PerformClick.run(View.java:27640)
  at android.os.Handler.handleCallback(Handler.java:883)
  at android.os.Handler.dispatchMessage(Handler.java:100)
  at android.os.Looper.loop(Looper.java:230)
  at android.app.ActivityThread.main(ActivityThread.java:7725)
  at java.lang.reflect.Method.invoke(Native method)
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:526)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1034)
   
  ........省略N行.....
   
  "WQW TEST" prio=5 tid=3 TimeWating
  | group="main" sCount=1 dsCount=0 flags=1 obj=0x12c44230 self=0x772f0ec000
  | sysTid=22938 nice=0 cgrp=default sched=0/0 handle=0x77391fbd50
  | state=S schedstat=( 274896 0 1 ) utm=0 stm=0 core=1 HZ=100
  | stack=0x77390f9000-0x77390fb000 stackSize=1039KB
  | held mutexes=
  at java.lang.Thread.sleep(Native method)
  - sleeping on <0x043831a6> (a java.lang.Object)
  at java.lang.Thread.sleep(Thread.java:440)
  - locked <0x043831a6> (a java.lang.Object)
  at java.lang.Thread.sleep(Thread.java:356)
  at com.example.test.MainActivity$onCreate$2$thread$1.run(MainActivity.kt:22)
  - locked <0x01aed1da> (a java.lang.Object)------------------------------------------------------------关键行!!!
  at java.lang.Thread.run(Thread.java:919)

这是一个典型的主线程被锁阻塞的例子; 其中等待的锁是<0x01aed1da>,这个锁的持有者是线程 3。进一步搜索 "tid=3" 找到线程3, 发现它正在TimeWating。

那么ANR的原因找到了:线程3持有了一把锁,并且自身长时间不释放,主线程等待这把锁发生超时。在线上环境中,常见因锁而ANR的场景是SharePreference写入。

学习资料

今日头条 ANR 优化实践系列分享 - 实例剖析集锦

今日头条 ANR 优化实践系列 - 监控工具与分析思路

今日头条 ANR 优化实践系列 - 设计原理及影响因素

相关推荐
Jouzzy5 小时前
【Android安全】Ubuntu 16.04安装GDB和GEF
android·ubuntu·gdb
极客先躯5 小时前
java和kotlin 可以同时运行吗
android·java·开发语言·kotlin·同时运行
Good_tea_h7 小时前
Android中的单例模式
android·单例模式
黑狼传说9 小时前
前端项目优化:极致最优 vs 相对最优 —— 深入探索与实践
前端·性能优化
Lill_bin11 小时前
Lua编程语言简介与应用
开发语言·数据库·缓存·设计模式·性能优化·lua
计算机源码社12 小时前
分享一个基于微信小程序的居家养老服务小程序 养老服务预约安卓app uniapp(源码、调试、LW、开题、PPT)
android·微信小程序·uni-app·毕业设计项目·毕业设计源码·计算机课程设计·计算机毕业设计开题
丶白泽13 小时前
重修设计模式-结构型-门面模式
android
晨春计14 小时前
【git】
android·linux·git
标标大人15 小时前
c语言中的局部跳转以及全局跳转
android·c语言·开发语言
竹林海中敲代码15 小时前
Qt安卓开发连接手机调试(红米K60为例)
android·qt·智能手机