当Android应用程序的UI线程阻塞时间过长时,会触发“应用程序不响应”(ANR)错误。如果应用程序处于前台,系统会向用户显示一个对话框,如图1所示。ANR对话框为用户提供强制退出应用程序的机会。
ANR是一个问题,因为应用程序的主线程(负责更新UI)不能处理用户输入的事件或绘制,从而导致用户感到沮丧。
当下列情况之一发生时,您的应用将触发ANR:
- 当你的活动处于前台时,你的应用程序
BroadcastReceiver
在5秒内没有响应输入事件或(例如按键或屏幕触摸事件)。 - 虽然在前台没有任何活动,但
BroadcastReceiver
在相当长的时间内还没有完成执行。 - 如果你的应用程序遇到ANR,则可以使用本文中的指导来诊断和解决问题。
检测和诊断问题
Android提供了多种方式让您知道你的应用有问题,并帮助你诊断。如果你已经发布了你的应用程序,Android维权人员可以提醒你问题正在发生,并且有诊断工具可帮助你查找问题。
Android的重要功能
Android的重要功能可以通过Play控制台提醒你,当你的应用显示过多的ANR时,可以帮助你提高应用的性能 。Android的重要功能认为应用程序的ANR过度:
- 至少在每日会议的0.47%中至少展示一次ANR。
- 展品2或更多的ANR至少在其日常会议的0.24%。
- 一个日常的会话是指在使用你的应用程序的日子。
诊断ANRs
在诊断ANR时有一些常见的模式可供选择:
- 该应用程序正在执行缓慢的操作涉及主线程上的I / O。
- 该应用程序在主线程上做了长时间的计算。
- 主线程正在对另一个进程执行同步联编程序调用,而另一个进程需要很长时间才能返回。
- 主线程被阻塞,等待一个正在另一个线程上发生的长操作的同步块。
- 主线程与另一个线程处于死锁状态,无论是在你的进程还是通过联编程序调用。主线不仅仅是等待长时间的操作才能完成,而是陷入了僵局。
以下技术可以帮助您找出哪些原因导致您的ANR。
strict模式
使用StrictMode帮助你在开发应用程序时在主线程上发现偶然的I / O操作。你可以StrictMode在应用程序或活动级别使用。
启用背景ANR对话框
Android仅在设备的“ 开发人员”选项中显示所有ANR的情况下,才会显示ANR对话框,用于处理广播消息所需的时间太长的应用程序。出于这个原因,背景ANR对话并不总是显示给用户,但应用程序仍然可能遇到性能问题。
Traceview
您可以使用Traceview在遍历用例的同时获取正在运行的应用程序的跟踪信息,并确定主线程繁忙的地方。
使用一个跟踪文件
Android /data/anr/traces.txt
在设备上的文件中遇到ANR
时会存储一些跟踪信息 。你可以通过以root用户身份在设备上启动shell会话来从仿真程序获取文件,如以下命令行示例所示:
adb root
adb shell
cat /data/anr/traces.txt
你可以使用设备上的“获取错误报告开发者”选项或开发计算机上的“adb bugreport”
命令来从物理设备捕获错误报告。
解决问题
在确定问题后,你可以使用本问文中的提示来解决常见问题。
主线程上的代码变慢
确定你的代码中应用程序的主线程忙于超过5秒钟的地方。在你的应用程序中查找可疑用例并尝试重现ANR。
例如,图2显示了Traceview时间线,其中主线程忙于超过5秒钟。
图2显示了大多数有问题的代码发生在onClick(View)处理程序中,如以下代码示例所示:
@Override
public void onClick(View view) {
// This task runs on the main thread.
BubbleSort.sort(data);
}
在这种情况下,你应该将在主线程中运行的工作移至工作线程。Android框架包括可以帮助将任务移动到工作线程的类,以下代码显示了如何使用AsyncTaskhelper类
来处理工作线程上的任务:
@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所示。主线程可用于响应用户事件。
主线程上的IO
在主线程上执行IO操作是导致主线程操作速度慢的一个常见原因,这会导致ANR。建议将所有IO操作移至工作线程。
IO操作的一些示例是网络和存储操作。
争用锁
在某些情况下,导致ANR的工作不会直接在应用程序的主线程上执行。如果工作者线程在主线程完成工作所需的资源上持有锁,则可能会发生ANR。
例如,图4显示了Traceview时间线,其中大部分工作是在工作线程上执行的。
但是,如果你的用户仍然遇到ANR,则应该查看Android设备监视器中主线程的状态。通常情况下,如果主线程已经RUNNABLE准备好更新用户界面,并且通常是响应的,那么主线程就处 但如果主线程无法恢复执行,则处于BLOCKED状态,无法响应事件。Android设备监视器上的状态显示为“ 监视”或“ 等待”,如图5所示。
以下跟踪显示了被阻止等待资源的应用程序主线程:
...
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)
...
查看跟踪可以帮助你找到阻塞主线程的代码。下面的代码负责持有阻塞前一个跟踪中的主线程的锁:
@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]);
}
}
}
另一个示例是应用程序的主线程正在等待工作线程的结果,如以下代码所示:
de
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,那么你应该看看为主线程所需的资源所持有的锁。
确保锁保持最少的时间,或者甚至更好,评估应用程序是否需要首先举行。如果使用锁来确定何时基于工作线程的处理来更新UI,请使用诸如onProgressUpdate()和之类的机制onPostExecute()来在工作线程和主线程之间进行通信。
死锁
当线程进入等待状态时会发生死锁,因为所需的资源由另一个线程持有,这也正在等待第一个线程占用的资源。如果应用程序的主线程在这种情况下,ANR可能会发生。
死锁在计算机科学中是一个研究得很好的现象,并且有可以用来避免死锁的死锁预防算法。
慢速广播接收机
应用程序可以通过广播接收器响应广播消息,例如启用或禁用飞行模式或更改连接状态。当应用程序花费太长时间来处理广播消息时,会发生ANR。
ANR发生在下列情况下:
- 广播接收机还没有
onReceive()
在相当长的时间内完成其方法。 - 广播接收者呼叫
goAsync()
并且不能呼叫finish()
该PendingResult
对象。 - 你的应用程序只能在
onReceive()
方法中执行短操作BroadcastReceiver
。但是,如果你的应用程序由于广播消息而需要更复杂的处理,则应将任务推迟到IntentService
。
你可以使用Traceview
等工具来确定你的广播接收器是否在应用的主线程上执行长时间运行的操作。例如,图6显示了在主线程上处理消息大约100秒的广播接收器的时间线。
此行为可以通过对该onReceive()
方法执行长时间运行的操作来引起BroadcastReceiver
,如以下示例所示:
@Override
public void onReceive(Context context, Intent intent) {
// This is a long-running operation
BubbleSort.sort(data);
}
在这样的情况下,建议将长时间运行的操作移动到一个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
在工作者线程而不是主线程上执行长时间运行的操作。图7显示了推迟到Traceview
时间线中的工作线程的工作。
你的广播接收器可以用goAsync()
来向系统发出信号表示它需要更多时间来处理消息。但是,你应该调用finish()
的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超时仍然适用。