一、什么是ANR?
ANR(Application Not responding),是指应用程序未响应,Android系统对于一些事件需要在一定的时间范围内完成,如果超过预定时间能未能得到有效响应或者响应时间过长,都会造成ANR。一般地,这时往往会弹出一个提示框,告知用户当前xxx未响应,用户可选择继续等待或者Force Close。
那么哪些场景会造成ANR呢?
- Service Timeout:前台服务在20s内未执行完成;
- BroadcastQueue Timeout:前台广播在10s内未执行完成
- ContentProvider Timeout:内容提供者在publish过超时10s;
- InputDispatching Timeout:输入事件分发超时5s,包括按键和触摸事件。
二、ANR原理
发生ANR时会调用AppNotRespondingDialog.show()方法弹出对话框提示用户,该对话框的依次调用关系:
AppErrors.appNotResponding(); //ANR对话框的唯一入口
AMS.UiHandler.sendMessage(ActivityManagerService.SHOW_NOT_RESPONDING_UI_MSG);
AppErrors.handleShowAnrUi();
AppNotRespondingDialog.show();
根据造成ANR的场景,产生ANR的来源可以总结为两大类:组件类ANR和Input ANR。
(一)Service超时机制
对于Service、Broadcast、Provider组件类的ANR而言,如果把发生ANR比作是引爆炸弹,那么整个流程包含三部分组成:
- 埋炸弹:中控系统(system_server进程)启动倒计时,在规定时间内如果目标(应用进程)没有干完所有的活,则中控系统会定向炸毁(杀进程)目标。
- 拆炸弹:在规定的时间内干完工地的所有活,并及时向中控系统报告完成,请求解除定时炸弹,则幸免于难。
- 引爆炸弹:中控系统立即封装现场,抓取快照,搜集目标执行慢的罪证(traces),便于后续调试分析,最后是炸毁目标。
bumpServiceExecutingLocked;
scheduleServiceTimeoutLocked; //监听Service运行时间
AMS.post(SERVICE_TIMEOUT_MSG); //记录executeingService并发送超时消息
ActiveServices.serviceTimeout; //判断是否超时
AppErrors.appNotResponding;
bumpServiceExecutingLocked 在很多地方被调用:
requestServiceBindingLocked(ServiceRecord, IntentBindRecord, boolean, boolean)
realStartServiceLocked(ServiceRecord, ProcessRecord, boolean)
sendServiceArgsLocked(ServiceRecord, boolean, boolean)
bringDownServiceLocked(ServiceRecord)
removeConnectionLocked(ConnectionRecord, ProcessRecord, ActivityRecord)
每个调用的地方都对应着Service的一个生命周期,也就是说Service的每个生命周期开始时都会调用到scheduleServiceTimeoutLocked用于监听Service运行的时间。
(二)输入事件超时机制
Input类型的ANR在日常开发中更为常见且更复杂,比如用户或者测试反馈,点击屏幕中的UI元素导致卡死。
与组件类ANR不同的是,Input类型的超时机制并非时间到了一定就会爆炸,而是处理后续上报事件的过程才会去检测是否该爆炸,所以更像是扫雷过程。
什么叫做扫雷呢,对于输入系统而言,即使某次事件执行时间超过预期的时长,只要用户后续没有再生成输入事件,那么也不需要ANR。而只有当新一轮的输入事件到来,此时正在分发事件的窗口(即App应用本身)迟迟无法释放资源给新的事件去分发,这时InputDispatcher才会根据超时时间,动态的判断是否需要向对应的窗口提示ANR信息。
InputDispatcher的源码实现中,整体的事件分发流程共使用到3个事件队列:
- inBoundQueue:用于记录InputReader发送过来的输入事件;
- outBoundQueue:用于记录即将分发给目标应用窗口的输入事件;
- waitQueue:用于记录已分发给目标应用,且应用尚未处理完成的输入事件。
1. 第一轮事件分发
首先InputReader线程 通过EventHub监听到底层的输入事件上报,并将其放入了inBoundQueue中,同时唤醒了InputDispatcher线程。
然后InputDispatcher开始了第一轮的事件分发,此时并没有正在处理的事件,因此InputDispatcher从inBoundQueue队列头部取出事件,并重置ANR的计时,并检查窗口是否就绪,此时窗口准备就绪,将该事件转移到了outBoundQueue队列中,因为应用管道对端连接正常,因此事件从outBoundQueue取出,然后放入了waitQueue队列,因为Socket双向通信已经建立,接下来就是 应用进程 接收到新的事件,然后对其进行分发。
如果应用进程事件分发正常,那么会通过Socket向system_server通知完成,则对应的事件最终会从waitQueue队列中移除。
2. 第二轮事件分发
如果第一轮事件分发尚未接收到回调通知,第二轮事件分发抵达又是如何处理的呢?
第二轮事件到达InputDispatcher时,此时InputDispatcher发现有事件正在处理,因此不会从inBoundQueue取出新的事件,而是直接检查窗口是否就绪,若未就绪,则进入ANR检测状态。
至此,输入系统检测到了ANR的发生,并向上层抛出了本次ANR的相关信息。
这里我们来总结一下,有哪些路径会引发ANR?
从埋下定时炸弹到拆炸弹之间的任何一个或多个路径执行慢都会导致ANR(以service为例),可以是service的生命周期的回调方法(比如onStartCommand)执行慢,可以是主线程的消息队列存在其他耗时消息让service回调方法迟迟得不到执行,可以是SP操作执行慢,可以是system_server进程的binder线程繁忙而导致没有及时收到拆炸弹的指令。
三、ANR调试
对于service、broadcast、provider、input发生ANR后,中控系统会马上去抓取现场的信息,用于调试分析。收集的信息包括如下:
1. 将am_anr信息输出到EventLog,也就是说ANR触发的时间点最接近的就是EventLog中输出的am_anr信息
2. 收集以下重要进程的各个线程调用栈trace信息,保存在 data/anr/traces.txt文件
- 当前发生ANR的进程,system_server进程以及所有persistent进程
- audioserver, cameraserver, mediaserver, surfaceflinger等重要的native进程
- CPU使用率排名前5的进程
3. 将发生ANR的reason以及CPU使用情况信息输出到main log
4. 将traces文件和CPU使用情况信息保存到dropbox,即data/system/dropbox目录
5. 对用户可感知的进程则弹出ANR对话框告知用户,对用户不可感知的进程发生ANR则直接杀掉
Demo:
mBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
mBtn2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mBtn2.setText("hah");
}
log文件:
2020-12-03 17:05:51.802 3105-3180/system_process E/ActivityManager: ANR in com.example.produceanr (com.example.produceanr/.MainActivity)
PID: 27806
Reason: Input dispatching timed out
//ANR 发生所处的 activity,进程ID,以及ANR原因
(com.example.produceanr/com.example.produceanr.MainActivity, Waiting to send non-key event because the touched window has not finished processing certain input events that were delivered to it over 500.0ms ago. Wait queue length: 2. Wait queue head age: 8559.5ms.)
Parent: com.example.produceanr/.MainActivity
Load: 6.09 / 5.99 / 5.74
CPU usage from 0ms to 6042ms later (2020-12-03 17:05:45.718 to 2020-12-03 17:05:51.760):
16% 3105/system_server: 7.9% user + 8.1% kernel / faults: 6549 minor
0.3% 2779/media.codec: 0.2% user + 0% kernel / faults: 35716 minor
10% 763/surfaceflinger: 8.2% user + 2.6% kernel / faults: 419 minor
...
0.1% 27263/kworker/4:2: 0% user + 0.1% kernel
0.1% 27743/kworker/0:0: 0% user + 0.1% kernel
15% TOTAL: 10% user + 5.4% kernel + 0.1% iowait + 0.2% irq + 0% softirq
//ANR前后cpu的使用情况
//如果CPU使用量接近100%,说明当前设备很忙,有可能是CPU饥饿导致ANR
//如果CPU使用量很少,说明主线程被block了
//如果IOwait很高,说明主线程在进行I/O操作
trace文件:
log 文件只是告诉你 ANR 发生时间,但是并具体详细的信息,这时候就得查看 trace 文件(App 的进程发生 ANR 时,系统让活跃的 Top 进程都进行了一下 dump,进程中的各种Thread 就都 dump 到这个 trace 文件里了,所以 trace 文件中包含了每一条线程的运行时状态)。
拉取trace文件:
adb pull data/anr/anr_2020-12-03-17-30-41-366
anr_2020-12-03-17-30-41-366.txt:
----- pid 28207 at 2020-12-03 17:30:41 -----
Cmd line: com.example.produceanr
//进程号、ANR发生时间和进程名称
DALVIK THREADS (14):
"main" prio=5 tid=1 Sleeping
//线程名、线程优先级、线程号、线程当前状态
| group="main" sCount=1 dsCount=0 flags=1 obj=0x76e543c8 self=0x6ffcf70000
| sysTid=28207 nice=-10 cgrp=default sched=0/0 handle=0x6ffe4d7ed0
| state=S schedstat=( 1106323008 54303290 408 ) utm=97 stm=13 core=4 HZ=100
| stack=0x7fcbd76000-0x7fcbd78000 stackSize=8192KB
| held mutexes=
at java.lang.Thread.sleep(Native method)
- sleeping on <0x0a2aeaaf> (a java.lang.Object)
at java.lang.Thread.sleep(Thread.java:440)
- locked <0x0a2aeaaf> (a java.lang.Object)
at java.lang.Thread.sleep(Thread.java:356)
at com.example.produceanr.MainActivity$1.onClick(MainActivity.java:23)
四、避免ANR
- 绝对不要在主线程上进行复杂耗时的操作,比如说发送接收网络数据、进行大量计算、操作数据库、读写文件等,统统采用异步操作
- Service中的耗时操作最好也是采用异步任务
- 在设计及代码编写阶段避免出现出现死锁、死循环等不恰当情况
- 一些避免、检测ANR的工具
- StrictMode:用来检测代码中是否存在违规操作的工具类(检测主线程是否存在耗时操作)
- BlockCanary:用来监控应用主线程的卡顿