一. ANR简介
描述:UI线程如果停止响应太长的时间, “Application Not Responding” (ANR) 就被触发。
- 如果被阻塞的app处于前台,系统会显示一个ANR对话框。
触发:以下两个条件,任意各一个都会导致ANR
- 当app处于前台时,在5s内无法相应用户输入或广播。
- 当app没有activity处于前台时,广播接收器正在进行长时间的任务,且无法结束。
二. 检测和诊断ANR
- 使用Android vitals。Play Console会在vital dashboard中显示ANR数据。
- 只要满足以下条件,Play Console会在vitals会认为 ANRs 过多:
- 最少每天session的0.47%出现至少一次ANR
- 最少每天session的0.24%出现2次或以上ANR
- 每天session是指每天App的使用次数
- 只要满足以下条件,Play Console会在vitals会认为 ANRs 过多:
- 诊断ANR,下面将列举一些典型的ANR原因
- 在主线程进行很慢的I/O操作
- 在主线程进行长时间的计算
- 在主线程进行同步的binder远程调用,而远程调用需要很长的事件才能返回
- 主线程阻塞,并等待其它线程执行长时间操作的同步块。
- 通过自己进程或远程方法调用,主线程被其它线程死锁。此时,主线程并非在等待长时间操作完成,而是处于死锁状态。
- 通过以下方法来查找发生ANR的具体原因
- 使用严格模式
Strict mode
,可以找到主线程中意外的I/O操作。可以在Application或Activity中使用。一般推荐在Application级就使用。 - 在开发者模式中,选择打开”显示所有ANR对话框”。默认情况下这个开关是关闭的,所有有时后台app发生了ANR,但不会显示ANR对话框。
- 使用Traceview,可以追踪app的运行轨迹,并确定主线程被卡住的地方。
- 在root情况下,可以从设备拉取traces文件
- 老版本中,文件位于/data/anr/traces.txt
- 较新的版本中,变成了多个文件/data/anr/anr_*
- 通过
adb bugreport
获取bug reports,或者从设备的开发者选项中获取。
- 使用严格模式
三. 解决问题
耗时计算:确定主线程中超过5秒操作的可疑代码,尝试重现ANR。Traceview timeline 可以直观的显示很忙的主线程操作。
- 确定长耗时的操作后,将这些操作以异步方式执行。可以使用
AsyncTask
、Thread
、线程池等方式来实现,保证主线程的交互响应。 - Traceview同样可以显示工作线程正在处理的任务
示例代码,修改前:
@Override public void onClick(View view) { // 耗时很长的冒泡排序 BubbleSort.sort(data); }
示例代码,修改后:
@Override public void onClick(View view) { // 将长耗时任务放在工作线程(子线程)中 new AsyncTask<Integer[], Integer, Long>() { @Override protected Long doInBackground(Integer[]... params) { BubbleSort.sort(params[0]); } }.execute(data); }
- 确定长耗时的操作后,将这些操作以异步方式执行。可以使用
IO操作:推荐将IO操作放到子线程中,就像上一条一样。
锁竞争:如果子线程对某资源加锁,而主线程同样需要这个资源,ANR就可能会发生。
- 锁竞争不一定会导致ANR。
- 由于锁竞争导致ANR发生时,主线程将处于阻塞状态。观察Traceview,可以发现主线的状态为
Monitor
或Wait
。 查看trace file示例,也显示了资源锁竞争的问题:
AsyncTask #2” prio=5 tid=18 Runnable
| group=”main” sCount=0 dsCount=0 obj=0x12c333a0 self=0x94c87100
| sysTid=25287 nice=10 cgrp=default sched=0/0 handle=0x94b80920
| state=R schedstat=( 0 0 0 ) utm=757 stm=0 core=3 HZ=100
| stack=0x94a7e000-0x94a80000 stackSize=1038KB > | held mutexes= “mutator lock”(shared held)
at com.android.developer.anrsample.BubbleSort.sort(BubbleSort.java:8)
at com.android.developer.anrsample.MainActivity$LockTask.doInBackground(MainActivity.java:147)locked <0x083105ee> (a java.lang.Boolean)
at com.android.developer.anrsample.MainActivity LockTask.doInBackground(MainActivity.java:135)atandroid.os.AsyncTask L o c k T a s k . d o I n B a c k g r o u n d ( M a i n A c t i v i t y . j a v a : 135 ) a t a n d r o i d . o s . A s y n c T a s k 2.call(AsyncTask.java:305)
at java.util.concurrent.FutureTask.run(FutureTask.java:237)
at android.os.AsyncTask SerialExecutor S e r i a l E x e c u t o r 1.run(AsyncTask.java:243)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1133)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)
at java.lang.Thread.run(Thread.java:761)示例代码,修改前:
``` @Override public void onClick(View v) { // 子线程在lockedResource上持有锁 new LockTask().execute(data); synchronized (lockedResource) { // 主线程在这里需求lockedResource // 但必须等待子线程结束... } } public class LockTask extends AsyncTask<Integer[], Integer, Long> { @Override protected Long doInBackground(Integer[]... params) { synchronized (lockedResource) { // This is a long-running operation, which makes // the lock last for a long time BubbleSort.sort(params[0]); } } } ```
示例代码,修改后:
``` @Override public void onClick(View v) { WaitTask waitTask = new WaitTask(); synchronized (waitTask) { try { waitTask.execute(data); // 等待子线程的通知 waitTask.wait(); } catch (InterruptedException e) {} } } class WaitTask extends AsyncTask<Integer[], Integer, Long>{ @Override protected Long doInBackground(Integer[]... params) { synchronized (this) { BubbleSort.sort(params[0]); // 完成,通知主线程 notify(); } } } ```
- 除了锁,还有其它的情况会导致主线程block。比如:
Semaphore
信号标- 资源池,比如数据库连接池
- 其它互斥机制
- 为了避免锁竞争导致的ANR,应当:
- 仔细检查主线程中对锁的请求
- 尽量缩短子线程持有锁的时间
- 评估是否需要持有锁。如果真的需要,应当使用合理的线程通信方法。
Deadlocks
- 当两个线程互相等待对方持有的资源,导致都进入等待状态,就发生了死锁。
- 如果UI线程和其它线程处于了死锁状态,很可能会发生ANR。
- 请使用各种预防死锁算法来解决问题。
慢广播:如果花费太长的时间用来处理广播消息时,会出现ANR。
- 以下两种情况会导致ANR:
onReceive()
执行时间太长。So不要在这个方法中执行耗时操作。- 广播接收器调用
goAsync()
,但没有调用PendingResult#finish()
- 如果需要对广播消息进行更复杂的处理,应当推迟到
IntentService
中去执行。 - 可以通过Traceview 来追踪耗时操作。
onReceive()
示例代码,修改前:@Override public void onReceive(Context context, Intent intent) { // 长耗时 BubbleSort.sort(data); }
onReceive()
示例代码,修改后:@Override public void onReceive(Context context, Intent intent) { // The task now runs on a worker thread. Intent intentService = new Intent(context, MyIntentService.class); context.startService(intentService); } public class MyIntentService extends IntentService { @Override protected void onHandleIntent(@Nullable Intent intent) { BubbleSort.sort(data); } }
广播接收器可以通过
goAsync
给系统发送一个请求更多时间来处理消息的信号,但在处理完成后,应该调用PendingResult#finish()
:final PendingResult pendingResult = goAsync(); new AsyncTask<Integer[], Integer, Long>() { @Override protected Long doInBackground(Integer[]... params) { // This is a long-running operation BubbleSort.sort(params[0]); pendingResult.finish(); } }.execute(data);
请注意,只能在主线程中使用。如果将这段代码放到子线程中,ANR问题仍会出现。
- 以下两种情况会导致ANR: