如果 Android 应用的界面线程处于阻塞状态的时间过长,会触发“应用无响应”(ANR) 错误。如果应用位于前台,系统会向用户显示一个对话框,如图 1 所示。ANR 对话框会为用户提供强行退出应用的选项。
图 1. 向用户显示的 ANR 对话框
ANR 是一个问题,因为负责更新界面的应用主线程无法处理用户输入事件或绘制操作,引起用户的不满。如需详细了解应用的主线程,请参阅进程和线程。
出现以下任何情况时,系统都会针对您的应用触发 ANR:
- 当您的 Activity 位于前台时,您的应用在 5 秒钟内未响应输入事件或
BroadcastReceiver
(如按键或屏幕轻触事件)。 - 虽然前台没有 Activity,但您的
BroadcastReceiver
用了相当长的时间仍未执行完毕。
如果您的应用遇到 ANR 错误,您可以按照本文中的指导来诊断并解决问题。
检测和诊断问题
Android 提供了一些方式,以便在您的应用有问题时让您知道,并帮助您进行诊断。如果您已发布应用,Android Vitals 可以在您的应用出现问题时提醒您,并且有一些诊断工具可帮助您发现问题。
Android Vitals
当您的应用出现 ANR 错误的次数过多时,Android Vitals 可通过 Play 管理中心提醒您,因此有助于改进应用性能。当应用出现以下情况时,Android Vitals 会认为 ANR 次数超出了正常范围:
- 至少有 0.47% 的每日工作时段出现了至少一次 ANR。
- 至少有 0.24% 的每日工作时段出现了至少两次 ANR。
每日工作时段是指应用在一天内被使用的时间。
要了解 Google Play 如何收集 Android Vitals 数据,请参阅 Play 管理中心文档。
诊断 ANR
诊断 ANR 时需要考虑以下几种常见模式:
- 应用在主线程上非常缓慢地执行涉及 I/O 的操作。
- 应用在主线程上进行长时间的计算。
- 主线程在对另一个进程进行同步 binder 调用,而后者需要很长时间才能返回。
- 主线程处于阻塞状态,为发生在另一个线程上的长操作等待同步的块。
- 主线程在进程中或通过 binder 调用与另一个线程之间发生死锁。主线程不只是在等待长操作执行完毕,而且处于死锁状态。如需更多信息,请参阅维基百科上的死锁。
以下方法可帮助您找出是以上哪种原因造成了 ANR。
严格模式
使用 StrictMode
有助于您在开发应用时发现主线程上的意外 I/O 操作。您可以在应用级别或 Activity 级别使用 StrictMode
。
启用后台 ANR 对话框
只有在设备的开发者选项中启用了显示所有 ANR 时,Android 才会针对花费过长时间处理广播消息的应用显示 ANR 对话框。因此,系统并不会始终向用户显示后台 ANR 对话框,但应用仍可能会遇到性能问题。
TraceView
您可以使用 TraceView 在查看用例时获取正在运行的应用的跟踪信息,并找出主线程繁忙的位置。如需了解如何使用 TraceView,请参阅使用 TraceView 和 dmtracedump 分析性能。
拉取跟踪信息文件
Android 会在遇到 ANR 时存储跟踪信息。在较低的操作系统版本中,设备上只有一个 /data/anr/traces.txt
文件。在较新的操作系统版本中,有多个 /data/anr/anr_*
文件。您可以使用 Android 调试桥 (ADB) 作为根,从设备或模拟器中获取 ANR 跟踪信息:
adb root
adb shell ls /data/anr
adb pull /data/anr/<filename>
您可以使用设备上的“生成错误报告”开发者选项或开发机器上的 adb bugreport 命令,从实体设备获取错误报告。如需了解详情,请参阅获取和阅读错误报告。
解决问题
找出问题后,您可以参考本节中的提示解决常见问题。
主线程上执行速度缓慢的代码
在您的代码中找出应用的主线程忙碌时间超过 5 秒的位置。在您的应用中查找可疑用例并尝试重现 ANR。
例如,图 2 显示的 TraceView 时间轴中,主线程的忙碌时间超过了 5 秒。
图 2. TraceView 时间轴中显示了一个忙碌的主线程
图 2 显示了大多数违规代码发生在 onClick(View)
处理程序中,如以下代码示例所示:
Kotlin
override fun onClick(v: View) { // This task runs on the main thread. BubbleSort.sort(data) }
Java
@Override public void onClick(View view) { // This task runs on the main thread. BubbleSort.sort(data); }
在这种情况下,您应该将主线程中运行的工作移至工作线程。Android Framework 中包含有助于将任务移至工作线程的类,如需了解详情,请参阅用于线程处理的辅助类。以下代码显示了如何使用 AsyncTask
辅助类处理工作线程上的任务:
Kotlin
override fun onClick(v: View) { // The long-running operation is run on a worker thread object : AsyncTask<Array<Int>, Int, Long>() { override fun doInBackground(vararg params: Array<Int>): Long? = BubbleSort.sort(params[0]) }.execute(data) }
Java
@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 显示大部分代码都在工作线程上运行,如图 3 所示。主线程有空闲来响应用户事件。
图 3. TraceView 时间轴中显示了由工作线程处理的工作
主线程上的 IO
在主线程上执行 IO 操作是导致主线程上操作速度缓慢的常见原因,主线程上操作速度缓慢会导致 ANR。建议将所有 IO 操作移至工作线程,如上一部分所示。
IO 操作示例包括网络和存储操作。如需了解详情,请参阅执行网络操作和保存数据。
锁争用
在某些情况下,导致 ANR 的工作并不是直接在应用的主线程上执行。如果某工作线程持有对某项资源的锁,而该资源是主线程完成其工作所必需的,这种情况下就可能会发生 ANR。
例如,图 4 显示的 TraceView 时间轴中,大部分工作是在工作线程上执行的。
图 4. TraceView 时间轴中显示了工作线程上正在执行的工作
但如果用户仍然会遇到 ANR,您应该在 Android Device Monitor 中查看主线程的状态。通常情况下,如果主线程已准备好更新界面并且总体上响应速度较快,则处于 RUNNABLE
状态。
但如果主线程无法继续执行,则它处于 BLOCKED
状态,并且无法响应事件。该状态在 Android Device Monitor 中会显示为“Monitor”或“Wait”,如图 5 所示。
图 5. 处于“Monitor”状态的主线程
以下跟踪信息显示了因等待资源而处于阻塞状态的应用主线程:
...
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)
...
查看跟踪信息有助于找到阻塞主线程的代码。以下代码持有的锁导致了前面的跟踪信息中的主线程阻塞:
Kotlin
override fun onClick(v: View) { // The worker thread holds a lock on lockedResource LockTask().execute(data) synchronized(lockedResource) { // The main thread requires lockedResource here // but it has to wait until LockTask finishes using it. } } class LockTask : AsyncTask<Array<Int>, Int, Long>() { override fun doInBackground(vararg params: Array<Int>): Long? = synchronized(lockedResource) { // This is a long-running operation, which makes // the lock last for a long time BubbleSort.sort(params[0]) } }
Java
@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]); } } }
另一个示例是应用的主线程在等待来自某工作线程的结果,如以下代码所示。请注意,不建议在 Kotlin 中使用 wait()
和 notify()
,Kotlin 有自己的并发操作处理机制。使用 Kotlin 时,应尽量使用 Kotlin 专用机制。
Kotlin
fun onClick(v: View) { val lock = java.lang.Object() val waitTask = WaitTask(lock) synchronized(lock) { try { waitTask.execute(data) // Wait for this worker thread’s notification lock.wait() } catch (e: InterruptedException) { } } } internal class WaitTask(private val lock: java.lang.Object) : AsyncTask<Array<Int>, Int, Long>() { override fun doInBackground(vararg params: Array<Int>): Long? { synchronized(lock) { BubbleSort.sort(params[0]) // Finished, notify the main thread lock.notify() } } }
Java
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
以及资源池(如数据库连接池)或其他互斥(互斥锁)机制的线程。
您应总体上评估应用对资源持有的锁,但如果您想避免 ANR,则应查看对主线程所需资源持有的锁。
请确保将持有锁的时间降到最少,或者最好从一开始就评估应用是否需要持有锁。如果您使用锁来确定何时根据工作线程的处理情况来更新界面,请使用 onProgressUpdate()
和 onPostExecute()
之类的机制在工作线程和主线程之间进行通信。
死锁
线程进入等待状态时会发生死锁,因为所需资源由另一个线程持有,而该线程也在等待第一个线程持有的资源。如果应用的主线程处于这种情况,很可能会发生 ANR。
计算机科学领域对死锁现象进行了充分研究,目前有一些死锁预防算法可用于避免死锁。
执行速度缓慢的广播接收器
应用可以通过广播接收器响应广播消息,例如启用或停用飞行模式或更改连接状态。如果应用处理广播消息的用时过长,就会发生 ANR。
以下情况下会发生 ANR:
- 广播接收器用了相当长的时间仍未执行完
onReceive()
。 - 广播接收器对
PendingResult
对象调用了goAsync()
,但未能调用finish()
。
您的应用应只在 BroadcastReceiver
的 onReceive()
方法中执行短操作。不过,如果您的应用因广播消息而需要进行更复杂的处理,则应将该任务推迟到 IntentService
。
您可以使用 TraceView 等工具来识别广播接收器是否在应用的主线程上执行长时间运行的操作。例如,图 6 显示了某广播接收器的时间轴,该接收器在主线程上处理消息用时大约 100 秒。
图 6. TraceView 时间轴中显示了主线程上的 BroadcastReceiver 工作
如果对 BroadcastReceiver
的 onReceive()
方法执行长时间运行的操作,可能会导致此行为,如以下示例所示:
Kotlin
override fun onReceive(context: Context, intent: Intent) { // This is a long-running operation BubbleSort.sort(data) }
Java
@Override public void onReceive(Context context, Intent intent) { // This is a long-running operation BubbleSort.sort(data); }
在此类情况下,我们建议将长时间运行的操作移至 IntentService
,因为它使用工作线程来执行其工作。以下代码显示了如何使用 IntentService
处理长时间运行的操作:
Kotlin
override fun onReceive(context: Context, intent: Intent) { Intent(context, MyIntentService::class.java).also { intentService -> // The task now runs on a worker thread. context.startService(intentService) } } class MyIntentService : IntentService("MyIntentService") { override fun onHandleIntent(intent: Intent?) { BubbleSort.sort(data) } }
Java
@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
,长时间运行的操作将在工作线程(而非主线程)上执行。图 7 在 TraceView 时间轴中显示了推迟到工作线程的工作。
图 7. TraceView 时间轴中显示了在工作线程上处理的广播消息
您的广播接收器可以使用 goAsync()
向系统表明它需要更多时间来处理消息。不过,您应该对 PendingResult
对象调用 finish()
。以下示例显示了如何调用 finish() 让系统回收广播接收器并避免 ANR:
Kotlin
val pendingResult = goAsync() object : AsyncTask<Array<Int>, Int, Long>() { override fun doInBackground(vararg params: Array<Int>): Long? { // This is a long-running operation BubbleSort.sort(params[0]) pendingResult.finish() return 0L } }.execute(data)
Java
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 超时仍然适用。
要详细了解 ANR,请参阅让您的应用随时能迅速响应。要详细了解线程,请参阅线程性能。