概述
无论我们是在 Android 设备的使用过程中还是 Android 开发过程中,基本都遇到过这样的情况,系统卡死,弹框提示应用无响应。也就是我们常说的 ANR – Application Not Responding。
理论知识
ANR 是 Android 系统设计的一种机制,我们常见的有下面三种 ANR:
- KeyDispatchTimeout:按键或触摸事件在特定时间内无响应
- BroadcastTimeout:BroadcastReceiver 在特定时间内无法处理完成
- ServiceTimeout:Service 在特定的时间内无法处理完成
- ContentProvider:ContentProvider 执行超时,这种比较少见
系统认定产生 ANR 的超时时间设置如下:
ActiveServices.java
1 | // How long we wait for a service to finish executing. |
ActivityManagerService.java
1 | // How long we allow a receiver to run before giving up on it. |
而产生 ANR 的原因一般有:
- 主线程耗时操作
- 主线程被其它线程 Block
- 被 Binder 对端 Block
- Binder 被占满导致主线程无法和 SystemServer 通信
- 得不到系统资源(CPU/RAM/IO):CPU占用率过高,负载过大,IO操作太多,导致主线程无法得到CPU时间片等系统资源去执行,而产生ANR.
从进程的角度看:
- 问题出在当前进程:
- 主线程本身耗时, 或则主线程的消息队列存在耗时操作;
- 主线程被本进程的其他子线程所blocked;
- 问题出在远端进程(一般是binder call或socket等通信方式)
上面的每种场景我们尽量都在后面的性能优化实战系列中挑选有代表性的来介绍。
日志获取
ANR 问题发生了,我们首先要获取日志,才能接下来进一步分析。
- 使用 Log Report App 抓取。
- 使用 adb bugreprot
- 使用 adb pull /data/anr/traces.txt
对于service、broadcast、provider、input发生ANR后,系统会马上去抓取现场的信息,用于调试分析。收集的信息包括如下:
- 将am_anr信息输出到EventLog,也就是说ANR触发的时间点最接近的就是EventLog中输出的am_anr信息。
- 收集以下重要进程的各个线程调用栈trace信息,保存在data/anr/traces.txt文件
- 当前发生ANR的进程,system_server进程以及所有persistent进程
- audioserver, cameraserver, mediaserver, surfaceflinger等重要的native进程
- CPU使用率排名前5的进程
- 将发生ANR的reason以及CPU使用情况信息输出到main log
- 将traces文件和CPU使用情况信息保存到dropbox,即data/system/dropbox目录
- 对用户可感知的进程则弹出ANR对话框告知用户,对用户不可感知的进程发生ANR则直接杀掉
整个ANR信息收集过程比较耗时,其中抓取进程的trace信息,每抓取一个等待200ms,可见persistent越多,等待时间越长。关于抓取trace命令,对于Java进程可通过在adb shell环境下执行kill -3 [pid]可抓取相应pid的调用栈;对于Native进程在adb shell环境下执行debuggerd -b [pid]可抓取相应pid的调用栈。对于ANR问题发生后的蛛丝马迹(trace)在traces.txt和dropbox目录中保存记录。
有了现场信息,可以调试分析,先定位发生ANR时间点,然后查看trace信息,接着分析是否有耗时的message、binder调用,锁的竞争,CPU资源的抢占,以及结合具体场景的上下文来分析,调试手段就需要针对前面说到的message、binder、锁等资源从系统角度细化更多debug信息。
小技巧
下面分析看一下各种日志对我们分析 ANR 的作用。
Log 名称 | 作用 | 关键词 |
---|---|---|
trace.txt | 记录了在发生ANR时刻系统各个线程的执行状态 | 搜索发生 anr 的进程名称 |
system.log | 包含ANR发生时间点信息、ANR发生前的CPU信息,还包含大量系统服务输出的信息 | “ANR in” |
main.log | 包含ANR发生前应用自身输出的信息,可供分析应用是否有异常;此外还包含输出的GC信息,可供分析内存回收的速度,判断系统是否处于低内存或内存碎片化状态 | |
event.log | 包含AMS与WMS输出的应用程序声明周期信息,可供分析窗口创建速度以及焦点转换情况 | “am_anr” |
kernel.log | 包含kernel打出的信息,LowMemoryKiller杀进程、内存碎片化或内存不足,mmc驱动异常都可以在这里找到。 |
拿到日志后,我们首先要确定 ANR 发生的时间点,可以通过 trace.txt 里面搜索进程的名字来查看,或者通过在 是system log里搜索 Anr in 来搜索,或者通过在 event log 里面搜索 am_anr 关键字来确定发生的时间。
system 分析
1 | 15:57:26.938 1380 1420 E ActivityManager: ANR in com.android.systemui:recents |
ActivityManager: Load: 5.01 / 6.61 / 5.59
这一行表示当时的 CPU 负载,另外我们还可以通过 adb shell uptime
命令来来获取设备的 CPU 负载:
1 | 20:08:58 up 5:52, 0 users, load average: 1.27, 0.77, 0.76 |
Load后面的三个数字的意思分别是1分钟、5分钟、15分钟内系统的平均负荷。当CPU完全空闲的时候,平均负荷为0;当CPU工作量饱和的时候,平均负荷为1,通过Load可以判断系统负荷是否过重。
经验法则是这样的:
当系统负荷持续大于0.7,你必须开始调查了,问题出在哪里,防止情况恶化。
当系统负荷持续大于1.0,你必须动手寻找解决办法,把这个值降下来。
当系统负荷达到5.0,就表明你的系统有很严重的问题。
更准确的CPU使用信息我们还可以通过 Log Report 目录中 dumpsys/dumpsys_cpuinfo 来查看。
1 | CPU usage from 0ms to 6626ms later |
这里表示 ANR 发生之后这段时间的 CPU 使用情况,有时还会有 ANR 发生之前 CPU 的使用情况:
1 | CPU usage from 3446ms to -6045ms ago |
1 | 49% 685/surfaceflinger: 33% user + 15% kernel / faults: 1437 minor 2 major |
上面这一段日志可以得到ANR发生的时候,Top进程的Cpu占用情况,user代表是用户空间,kernel是内核空间。对于 CPU 的占用率,在多核中每个核最大占用率都是100%,如果机器是8核的,那么每个进程的CPU最大占用率就是800%,这也就是为什么我们会经常看到某些进程 CPU 占用率会大于100%,其实百分之一百多的CPU占用率不算很高的。
iowait 系统因为 io 导致的进程 wait,如果这部分数值较高,可能要查一下是不是 io 问题导致的 anr,有可能在主线程中进行了 io 操作。
分析这一部分,一般的有如下的规律。
- kswapd0 cpu占用率偏高,系统整体运行会缓慢,从而引起各种ANR。把问题转给”内存优化”,请他们进行优化。
- logd CPU占用率偏高,也会引起系统卡顿和ANR,因为各个进程输出LOG的操作被阻塞从而执行的极为缓慢。
- Vold占用CPU过高,会引起系统卡顿和ANR,请负责存储的同学先调查
- qcom.sensor CPU占用率过高,会引起卡顿,请系统同学调查
- 应用自身CPU占用率较高,高概率应用自身问题
- 系统CPU占用率不高,但主线程在等待一个锁,高概率应用自身问题
- 应用处于D状态,发生ANR,如果最后的操作是refriger,那么是应用被冻结了,正常情况下是功耗优化引起的
trace 分析
要查看应用被阻塞的位置,还是需要分析 trace 文件。
下面通过一个人为制造的 ANR 事件来讲解一下 traces 的分析技巧。
traces 文件在 /data/anr/traces.txt 可以找到,一般 ANR 发生时,在弹出无响应对话框之前,AMS 会把 ANR 相关的信息保存在这个文件中,因此,这个文件是分析 ANR 的利器。
1 | Cmd line: com.example.heqiang.testsomething |
main 线程分析
第一行: "main" prio=5 tid=1 Sleeping
这一行的信息主要包括:
- 线程名称(main)
- 线程的优先级(prio=5)
- 线程id(tid=1)
- 线程状态(Sleeping)。
线程状态在 art/runtime/thread_state.h 文件中,主要有下面的状态:
1 | enum ThreadState { |
第二行:| group="main" sCount=1 dsCount=0 obj=0x75c5c348 self=0x7519295a00
主要信息包括:
- 线程所处的线程组(group=”main”)
- 线程被正常挂起的次处(sCount=1)
- 线程因调试而挂起次数(dsCount=0),当一个进程被调试后,sCount会重置为0,调试完毕后sCount会根据是否被正常挂起增长,但是dsCount不会被重置为0,所以dsCount也可以用来判断这个线程是否被调试过
- 当前线程所关联的java线程对象(obj=0x75c5c348)
- 该线程本身的地址(self=0x7519295a00)。
第三行:| sysTid=8141 nice=0 cgrp=default sched=0/0 handle=0x751d25fa98
主要信息包括线程调度信息,分别是:
- 线程在linux系统下的内核线程id (sysTid=8141)
- 线程的调度有优先级(nice=0),nice值越小,优先级越高
- 调度策略(sched=0/0)
- 优先组属(cgrp=default)
- 处理函数地址(handle=0x751d25fa98)。
第四行:| state=S schedstat=( 173407732 13795308 237 ) utm=14 stm=2 core=5 HZ=100
主要信息包括当前线程的上下文信息:
- 分别是线程调度状态(state=S)S表示Sleeping,另外还有 “R (running)”, “S (sleeping)”, “D (disk sleep)”, “T (stopped)”, “t (tracing stop)”, “Z (zombie)”, “X (dead)”, “x (dead)”, “K (wakekill)”, “W (waking)”,),通常一般的Process 处于的状态都是S (sleeping), 而如果一旦发现处于如D (disk sleep), T (stopped), Z (zombie) 等就要认真审查。
- schedstat=( 173407732 13795308 237 ) 从 /proc/[pid]/task/[tid]/schedstat读出,三个值分别表示线程在cpu上执行的时间、线程的等待时间和线程执行的时间片长度,有的android内核版本不支持这项信息,得到的三个值都是0
- 线程用户态下使用的时间值(单位是jiffies)(utm=14)
- 内核态下得调度时间值(stm=2)
- 最后运行该线程的cup核的序号(core=5);
第五行:| stack=0x7ff4d74000-0x7ff4d76000 stackSize=8MB
主要信息包括:
- 线程栈的地址(stack=0x7ff4d74000-0x7ff4d76000)
- 栈大小(stackSize=8MB);
再后面就是线程的调用栈信息,也是我们分析ANR的关键信息。
由于上面的log是在主线程中调用 Thread.sleep 导致的,直接通过调用栈信息就可以定位到问题发生的原因,这里不再介绍。
Memory 信息分析
这里介绍了进程的内存使用情况,有时可以辅助我们分析 ANR。
1 | Total number of allocations 50059 //进程创建到现在一共创建了多少对象 |
1 | Heap: 0% free, 191MB/192MB; 3566339 objects |
关注一下内存使用以及 GC 的情况,如果内存占用很高,GC的时间很长,那么有可能是内存泄漏等原因导致。
CPU 占用分析
如果 ANR 发生时你发现有个应用占用 CPU 很高,导致发生 ANR 应用的UI线程拿不到 CPU, 从而导致 ANR 发生。
io 分析
如果 anr 时你发现 iowait 数值很高(在 30% 以上),这时要注意了,需要分析一下是不是由于 io 操作导致了 anr,看主线程中是否有进行 io 操作。
看看当前的调用堆栈,或者在这段ANR点往前看0~4s,看看当时做的什么文件操作,这种场景有遇到过,常见解决方法是对耗时文件操作采取异步操作。
假如和有关情况,一般blocked的位置应该是在io文件操作上。
iowait 就是系统因为 io 导致的进程 wait。这时候系统在做 io ,导致没有进程在干活,cpu 在执行 idle 进程空转,所以说 iowait 的产生要满足两个条件,一是进程在等 io ,二是等 io 时没有进程可运行。
iowait = (cpu idle time)/(all cpu time)。
如果想实时查看 IO 读写情况,可以使用 vmstat 命令,此命令还可以查看CPU使用率,内存使用,虚拟内存交换情况等。
分析binder调用
binder 对端超时
下面我们制造一个 binder 调用对端耗时操作导致的 anr。
看trace文件,main线程正在进行binder调用。
1 | "main" prio=5 tid=1 Native |
假如我们不知道对端是什么进程的情况下,可以分析 binderinfo 文件,这个文件一般在 /data/anr 目录中,类似 BinderTraces_19401_com.example.heqiang.testsomething:aidl.txt
这样命名。
1 | proc 19401 |
可以看到通信的binder进程号为 19307,线程号为 19320。
接下来就可以分析一下 19307 进程,看看 19320 线程进行的一些操作。
IPCThreadState::talkWithDriver
一般表示在等待对端进程的响应。
binder 线程池占满
另外,如果 Binder 线程池被占满(16个),导致主线程处理不了其他的 binder 请求,从而导致 anr。
判断Binder是否用完,可以在trace中搜索关键字”binder_f”,如果搜索到则表示已经用完,然后就要找log其他地方看是谁一直在消耗binder或者是有死锁发生。
线程状态分析
这里多说两句线程状态的问题,有时我们经常在线程的堆栈中看到这样的信息:
1 | at java.lang.Object.wait!(Native method) |
这里需要解释一下,为什么先 lock 了这个对象,然后又 waiting on 同一个对象呢?让我们看看这个线程对应的代码:
1 | synchronized(obj) { |
线程的执行中,先用 synchronized 获得了这个对象的 Monitor(对应于 locked <0x0b2886e9> )。当执行到 obj.wait(), 线程即放弃了 Monitor的所有权,进入 “wait set”队列(对应于 waiting on <0x0b2886e9> )。
往往在你的程序中,会出现多个类似的线程,他们都有相似的 DUMP信息。这也可能是正常的。比如,在程序中,有多个服务线程,设计成从一个队列里面读取请求数据。这个队列就是 lock以及 waiting on的对象。当队列为空的时候,这些线程都会在这个队列上等待,直到队列有了数据,这些线程被 Notify,当然只有一个线程获得了 lock,继续执行,而其它线程继续等待。0x0b2886e9>0x0b2886e9>
一般分析套路
发生ANR时从trace来看主线程却处于空闲状态或者停留在非耗时代码的原因有哪些?可以是抓取trace过于耗时而错过现场,可以是主线程消息队列堆积大量消息而最后抓取快照一刻只是瞬时状态,可以是广播的“queued-work-looper”一直在处理SP操作。