翻译的官方文章 原文链接
长时间阻塞Android应用的UI线程时会触发“Application Not Responding”(ANR)错误。如果应用处于前台,那么系统将会显示下图所示的ANR对话框。这个ANR对话框使得用户有机会去强制退出应用程序。
Android应用的主线程就是负责更新应用界面的线程,ANR错误就是这个主线程无法处理用户输入事件或者更新界面导致的,从而导致不好的用户体验。想了解更多关于应用主线程信息,请查看官方文档Processes and Threads。
如下几种情况会导致应用ANR:
- 当应用界面处于前台时,应用在5秒内没有处理用户输入事件或者广播接收事件(BroadcastReceiver);
- 当应用界面处于后台时,广播接受事件(BroadcastReceiver)在10秒内没有处理完成。
应用如果发生了ANR,我们可以按照这篇文章的指导来查找和修复问题。
检测和诊断ANR
Android 提供了一些工具让我们发现问题,并帮助我们诊断问题。如果应用已经发布了,那么当问题发生时Android vitals会提醒开发者,并且还有一些诊断工具帮助定位问题。
Android vitals
Android vitals的问题反馈能力可以帮助我们改善应用的性能。当应用频繁出现ANR时,Android vitals会反馈相关信息,反馈的信息在Play Console可以查看。一下情况会被认为是频繁ANR:
- 一天里使用过应用的用户中有0.47%的用户出现了至少一次ANR;
- 一天里使用过应用的用户中有0.24%的用户出现了两次及其以上的ANR。
想了解更多关于Google Play如果如何收集Android vitals数据信息的请查看Play Console文档。
诊断ANR
诊断ANR时通常检查如下几项:
- 应用在主线程中执行了涉及I/O的耗时操作;
- 应用在主线程中执行耗时的计算;
- 应用在主线程调用了一个同步binder接口,另一个进程长时间没有返回;
- 主线程等待的锁长时间被其他线程持有;
- 主线程与其他线程形成死锁。
下面的方法可以帮助我们分析出具体是哪种原因导致ANR。
Strict mode
在开发阶段使用StrictMode可以帮助我们找出意外在主线程执行了I/O操作的地方。我们可以在Application或者Activity层面使用StrictMode。
允许后台应用ANR显示对话框
只有在开发者选项中打开了“为后台应用显示ANR对话框”的开关,Android系统才会在应用长时间处理广播事件时显示ANR对话框。因为这个原因,后台引用的ANR对话框很可能不会显示,但实际APP任然存在性能问题。
Traceview
我们可以重复测试用例,使用Traceview来跟踪APP的运行情况,定位到主线程繁忙的地方。想了解更多关于Traceview的信息,请查看Profiling with Traceview and dmtracedump。
获取trace文件
当发生ANR时Android系统会将trace信息保存到文件。在老的系统版本上所有ANR的trace信息是保存在/data/anr/traces.txt。在新的系统版本上trace信息是保存在多个如/data/anr/anr_*文件中。我们可以使用如下adb命令从设备或者模拟器中获取trace文件:
adb root
adb shell ls /data/anr
adb pull /data/anr/<filename>
我们可以从物理设备上通过使用开发者选项中的提交错误报告选项获取错误报告,或者通过adb bugreport命令获取。详细信息查看 Capture and read bug reports。
修复ANR
定位了问题以后可以使用下面的方法来修复问题。
主线程中的耗时代码
定位代码中耗时的地方。找打可以的复现用例,然后尝试复现ANR。如下图所示,Traceview的时间线展示了主线程耗时的地方。
从上图可以看出最耗时的代码是在
onClick(View)
中,即如下所示代码:
@Override
public void onClick(View view) {
// This task runs on the main thread.
BubbleSort.sort(data);
}
在这个例子中,我们应该把耗时操作从主线程移到工作线程中去。Android系统框架中有许多类可以帮助我们将耗时操作移到工作线程中区,详细参看 Helper classes for threading。下面的代码展示使用 AsyncTask来完成这个工作。
@Override
public void onClick(View view) {
// The long-running operation is run on a worker thread
new AsyncTask<Integer[], Integer, Long>() {
@Override
protected Long doInBackground(Integer[]... params) {
BubbleSort.sort(params[0]);
}
}.execute(data);
}
下图是耗时代码放在工作线程后Traceview的截图,现在主线程可以响应用户输入事件了。IO在主线程
主线程耗时操作最常见的情况就是在主线程中执行了IO操作。推荐的做法是将IO操作移动到工作线程中,如上一部分所示。
IO常见的是网络请求和本地存储操作。更多信息可以参看 Performing network operations 和 Saving data。
锁竞争
有时候导致ANR的代码不是主线程中。如果工作线程持有了主线程请求的锁,这就可能出现ANR。
上图所示大多数执行时间是在工作线程中。如果这种情况下还是出现了ANR,你应该在Android Device Monitor中查看主线程的状态。正常情况主线程是在 RUNNABLE 状态。如果主线程在 BLOCKED 状态,不能响应用户事件。如下Android Device Monitor截图。
下面的trace信息展示了主线程等待锁资源阻塞的情况。
...
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)
at android.os.AsyncTask$2.call(AsyncTask.java:305)
at java.util.concurrent.FutureTask.run(FutureTask.java:237)
at android.os.AsyncTask$SerialExecutor$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)
...
查看trace文件可以帮助我们定位主线程中阻塞的位置。下面代码展示了上面trace文件中主线程请求锁资源阻塞了。
@Override
public void onClick(View v) {
// The worker thread holds a lock on lockedResource
new LockTask().execute(data);
synchronized (lockedResource) {
// The main thread requires lockedResource here
// but it has to wait until LockTask finishes using it.
}
}
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]);
}
}
}
另一个例子是主线程一直等待工作线程结果的情况。
public void onClick(View v) {
WaitTask waitTask = new WaitTask();
synchronized (waitTask) {
try {
waitTask.execute(data);
// Wait for this worker thread’s notification
waitTask.wait();
} catch (InterruptedException e) {}
}
}
class WaitTask extends AsyncTask<Integer[], Integer, Long> {
@Override
protected Long doInBackground(Integer[]... params) {
synchronized (this) {
BubbleSort.sort(params[0]);
// Finished, notify the main thread
notify();
}
}
}
还有一些阻塞主线程的情况,包括使用 Lock,Semaphore,资源池(数据库连接池)或者其他互斥机制。
一般我们都会评估APP使用的锁资源。但我们想要必要ANR,就必须更加注意主线程使用到的锁资源。
确定锁持有的时间不要过长,甚至评估是否需要使用锁。如果你使用锁来判断根据工作线程的进度来跟新UI,那么可以使用如 onProgressUpdate() 和 onPostExecute() 机制赖在工作线程和主线程间通信。
死锁
当A线程等待B线程所持有的锁1,而B线程却在等待A线程持有的锁2时就发生了死锁。这种情况ANR可能发生。死锁在计算机科学中已经研究的很充分了,我们可以使用死锁预防算法来避免死锁。更多信息可以参看 Deadlock 和 Deadlock prevention algorithms。
广播接收器中的耗时操作
APP能通过广播接收器处理广播消息,如飞行模式切换广播或者网络连接状态改变广播。当APP花了太多的时间处理广播消息时也会出现ANR。广播接收器可能有如下几种情况导致ANR:
- 广播接收器在相当长的时间内没有完成 onReceive() 方法;
- 广播接收器调用了 goAsync() 但是没有调用 PendingResult 对象的finish()方法。
APP在广播接收器的onReceive()方法中只应该做简单处理。如果确实需要执行复杂的逻辑,应该将任务发送到 IntentService 中去处理。
我们可以使用Traceview去检查广播接收器是否在主线程中执行了一个耗时操作。下图展示了在广播接收器中执行了大约100秒耗时操作的情况。
下图是一个在广播接收器中执行耗时操作的代码实例。
@Override
public void onReceive(Context context, Intent intent) {
// This is a long-running operation
BubbleSort.sort(data);
}
这种情况官方推荐将耗时操作移动到IntentService中去执行,IntentService会使用工作线程去完成任务。下面是使用IntentService的代码示例:
@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);
}
}
使用IntentService后耗时操作就从主线程转移到了工作线程了。下面是修改后Traceview的截图。
广播接收器可以使用 goAsync() 通知系统,当前广播事件需要更多的时间去处理。此外我们需要调用PendingResult对象的finish()方法,以此来告诉系统回收广播接收器,从而避免ANR。
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);
如果广播在后台,即使我们把耗时的操作移动到工作线程或者使用goAsync()方法也不能避免ANR。
关于更多关于ANR信息可以参看 Keeping your App Resposive。更多关于线程的信息可以参看 Threading Performance 。