一、ANR简介
ANR全称:Application Not Responding,也就是应用程序无响应。
Android系统中,ActivityManagerService(简称AMS)和WindowManagerService(简称WMS)会检测App的响应时间,如果App在特定时间无法相应屏幕触摸或键盘输入时间,或者特定事件没有处理完毕,就会出现ANR。
以下四个条件都可以造成ANR发生:
InputDispatching Timeout:5秒内无法响应屏幕触摸事件或键盘输入事件
BroadcastQueue Timeout :在执行前台广播(BroadcastReceiver)的onReceive()函数时10秒没有处理完成,后台为60秒。
Service Timeout :前台服务20秒内,后台服务在200秒内没有执行完毕。
ContentProvider Timeout :ContentProvider的publish在10s内没进行完。
1.1 避免
尽量避免在主线程(UI线程)中作耗时操作。
那么耗时操作就放在子线程中。
关于多线程可以参考:Android多线程:理解和简单使用总结
二、ANR分析办法
2.1 ANR重现
这里使用的是号称Google亲儿子的Google Pixel xl(Android 8.0系统)做的测试,生成一个按钮跳转到ANRTestActivity,在后者的onCreate()中主线程休眠20秒:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_anr_test);
// 这是Android提供线程休眠函数,与Thread.sleep()最大的区别是
// 该使用该函数不会抛出InterruptedException异常。
SystemClock.sleep(20 * 1000);
}
二、ANR出现场景
发生ANR时会调用AppNotRespondingDialog.show()方法弹出对话框提示用户,该对话框的依次调用关系如下图所示:
AppErrors.appNotResponding(),该方法是最终弹出ANR对话框的唯一入口,调用该方法的场景才会有ANR提示,也可以认为在主线程中执行无论再耗时的任务,只要最终不调用该方法,都不会有ANR提示,也不会有ANR相关日志及报告;通过调用关系可以看出哪些场景会导致ANR,有以下四种场景:
- InputDispatching Timeout:5秒内无法响应屏幕触摸事件或键盘输入事件。
-
BroadcastQueue Timeout :在执行前台广播(BroadcastReceiver)的
onReceive()
函数时10秒没有处理完成,后台为60秒。 - Service Timeout :前台服务20秒内,后台服务在200秒内没有执行完毕。
- ContentProvider Timeout :ContentProvider的publish在10s内没进行完。
三、出现ANR原因
- 主线程慢代码
- 主线程IO
- 锁竞争
- 死锁
四、 如何避免ANR
1.UI线程尽量只做跟UI相关的工作;
2.耗时的工作(比如数据库操作,I/O,连接网络或者别的有可能阻碍UI线程的操作)把它放入单独的线程处理;
3.尽量用Handler来处理UI thread和别的thread之间的交互;
4.实在绕不开主线程,可以尝试通过Handler延迟加载;
5.广播中如果有耗时操作,建议放在IntentService中去执行,或者通过goAsync() + HandlerThread分发执行。
五、分析ANR的重点
1.cpu占用率方面:
可以通过分析各进程的CPU时间占用率,来判断是否为某些进程长期占用CPU导致该进程无法获取到足够的CPU处理时间,而导致ANR重点关注下CPU的负载,各个进程总的CPU时间占用率,用户CPU时间占用率,核心态CPU时间占用率,以及iowait CPU时间占用率。
2.内存方面
主要看当前应用native和dalvik层内存使用情况,结合系统给每个应用分配的最大内存来分析。
ANR日志分析
当app
出现ANR时
会在data/anr/
目录下生成traces.txt
日志文件。每次发生ANR
时都会删除旧的traces
文件,重新创建新文件。也就是说Android
只保留最后一次发生ANR
时的信息。
首先,我们可以使用adb命令
导出traces文件
:
adb pull /data/anr/traces.txt d:\
友情提示:traces.txt
默认会被导出到Android SDK的\platform-tools目录。
开发中最方便的是在log里面就可以看到ANR的相关信息,以下面的日志为例,我们可以从Android studio logcat
很明显的看出ANR
发生的原因,用户的输入超时了,问题线程的PID:879。
同时我们还可以通俗易懂的看出来 CPU
平均负载,CPU
的使用情况:
4.67 ,3.32 ,1.49 分别表示 发生ANR
前一分钟,五分钟,十五分钟 CPU
的平均负载 Load: 4.67 / 3.32 / 1.49 CPU usage from 6021ms to 79ms ago。
接下来还是回到进一步分析traces.txt文件上来,看文件里面的内容:
----- pid 879 at 2019-01-02 08:05:04 -----Cmd line: com.sandiyu.lcd JNI: CheckJNI is off; workarounds are off; pins=2; globals=273 DALVIK THREADS:(mutexes: tll=0 tsl=0 tscl=0 ghl=0) "main" prio=5 tid=1 WAIT | group="main" sCount=1 dsCount=0 obj=0x4159cd68 self=0x414d6510 | sysTid=879 nice=0 sched=0/0 cgrp=apps handle=1074020692 | state=S schedstat=( 0 0 0 ) utm=602 stm=168 core=1 at java.lang.Object.wait(Native Method) - waiting on <0x4159ce38> (a java.lang.VMThread) held by tid=1 (main) at java.lang.Thread.parkFor(Thread.java:1205) at sun.misc.Unsafe.park(Unsafe.java:325) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:157) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2017) at java.util.concurrent.LinkedBlockingQueue.put(LinkedBlockingQueue.java:318) at com.sandiyu.lcd.utils.DeviceCommandSender$CommandSendThread.send(DeviceCommandSender.java:156) at com.sandiyu.lcd.utils.DeviceCommandSender.displayNull(DeviceCommandSender.java:81) at com.sandiyu.lcd.DlpPrintActivity$PrintRunnable.clearImage(DlpPrintActivity.java:884) at com.sandiyu.lcd.DlpPrintActivity$PrintRunnable.access$1900(DlpPrintActivity.java:253) at com.sandiyu.lcd.DlpPrintActivity.onBackPressed(DlpPrintActivity.java:954) at android.app.Activity.onKeyUp(Activity.java:2193) ...
一般trace
文件顶部的线程即为ANR
的元凶,找到了犯罪线程我们就可以查看、分析一下犯罪现场。
- line 1,2
----- pid 879 at 2019-01-02 08:05:04 -----Cmd line: com.sandiyu.lcd
可以看到ANR
发生的进程id,时间,名称。
- line 3,4,5
JNI: CheckJNI is off; workarounds are off; pins=2; globals=273 DALVIK THREADS:(mutexes: tll=0 tsl=0 tscl=0 ghl=0)
可以看到线程的基本信息(tll:thread list lock,tsl:thread suspend lock,tscl:thread suspend count lock,ghl:gc heap lock)。
- line "main"
"main" prio=5 tid=1 WAIT
这一行说明了线程名称,优先级,线程锁id和线程状态。可以看到本次ANR
线程为WAIT
状态。
额外补充一下线程状态有如下几种:
java thread 状态 | cpp thread状态 | 说明 |
---|---|---|
TERMINATED | ZOMBIE | 线程死亡,终止运行 |
RUNNABLE | RUNNING/RUNNABLE | 线程可运行或正在运行 |
TIMED_WAITING | TIMED_WAIT | 执行了带有超时参数的wait、sleep或join函数 |
BLOCKED | MONITOR | 线程阻塞,等待获取对象锁 |
WAITING | WAIT | 执行了无超时参数的wait函数 |
NEW | INITIALIZING | 新建,正在初始化,为其分配资源 |
NEW | STARTING | 新建,正在启动 |
RUNNABLE | NATIVE | 正在执行JNI本地函数 |
WAITING | VMWAIT | 正在等待VM资源 |
RUNNABLE | SUSPENDED | 线程暂停,通常是由于GC或debug被暂停 |
UNKNOWN | 未知状态 |
接着往下面的信息看
at com.sandiyu.lcd.utils.DeviceCommandSender$CommandSendThread.send(DeviceCommandSender.java:156) at com.sandiyu.lcd.utils.DeviceCommandSender.displayNull(DeviceCommandSender.java:81) at com.sandiyu.lcd.DlpPrintActivity$PrintRunnable.clearImage(DlpPrintActivity.java:884) at com.sandiyu.lcd.DlpPrintActivity$PrintRunnable.access$1900(DlpPrintActivity.java:253) at com.sandiyu.lcd.DlpPrintActivity.onBackPressed(DlpPrintActivity.java:954)
在这里我们就找到了原因,CommandSendThread.send
需要等待网络资源来更新UI
,连接中断了,这时候点击onBackPressed
长时间得不到相应,它就报了ANR了。
六、造成ANR的原因及解决办法
上面例子只是由于简单的主线程耗时操作造成的ANR,造成ANR的原因还有很多:
主线程阻塞或主线程数据读取
解决办法:避免死锁的出现,使用子线程来处理耗时操作或阻塞任务。尽量避免在主线程query provider、不要滥用SharePreferenceS
CPU满负荷,I/O阻塞
解决办法:文件读写或数据库操作放在子线程异步操作。
内存不足
解决办法:AndroidManifest.xml文件中可以设置 android:largeHeap=“true”,以此增大App使用内存。不过不建议使用此法,从根本上防止内存泄漏,优化内存使用才是正道。
各大组件ANR
各大组件生命周期中也应避免耗时操作,注意BroadcastReciever的onRecieve()、后台Service和ContentProvider也不要执行太长时间的任务。