注:本文是对Android官网的ANR进行的翻译,个别不关紧要的描述就没翻译了,但总体意思与原文一致,原文链接如下:

Android ANR

(文章里面的“工作线程”其实就是指子线程)

首先,我们先来看下Android官方对ANR的定义:

When the UI thread of an Android app is blocked for too long, an "Application Not Responding" (ANR) error is triggered. If the app is in the foreground, the system displays a dialog to the user.

意思就是说,当一个应用程序的UI线程被长时间阻塞时,一个ANR(Application Not Responding,应用程序无响应)错误被触发。如果这个应用程序在前台,系统会向用户显示一个对话框(至于对话框长什么样,想必遇到过ANR的人都知道,我这里就不贴出来了)

我们要明确,ANR是一个非常严重的问题,因为APP的主线程(负责更新UI的线程)无法处理用户输入事件或绘制,对用户造成极大困扰。

当以下条件中的其中一个发生时,会出现ANR:

  1. 当activity位于前台时,你的程序在5秒内没有对输入事件或BroadcastReceiver做出响应(比如说按键、屏幕触摸事件)
  2. 当activity不处于前台时,你的BroadcastReceiver在相当长的时间内没有执行完成

如何诊断ANR

有一些普遍的场景如下:

  1. 程序在主线程做一些涉及 I/O 的操作
  2. 程序在主线程做一些长时间的运算
  3. 主线程对其他进程有一个同步的Binder调用,但其他进程需要花
  4. 很长时间才返回
  5. 主线程在等待一个长时间的同步锁而被阻塞,而这个长时间的操作位于另一个线程
  6. 主线程和其他线程在同一进程或通过Binder调用发生交互时,主线程出现死锁。此时,主线程不仅仅是在等待一个长时间的操作完成,而且还处于死锁状态

以下方法可以帮你找到是以上哪个原因导致ANR:

(1)启用 Strict mode

当你开发你的程序时,使用 StrictMode 可以帮你找到主线程内的一些意外IO操作,你可以在 application或activity级别内使用 StrictMode。

(2)启用后台ANR对话框

只有当设备的“开发者选项”里面的“显示所有ANR”这个开关被启用时,Android才会为那些花了长时间去处理广播消息的APP显示ANR对话框。所以,后台ANR对话框并不会总是显示,但是这个APP仍然在经历性能问题。

(3)Traceview

当你的程序在跑用例的时候,你可以使用 Traceview 来获得正在运行的这个程序的 trace 信息,来确认主线程繁忙的位置。关于如何使用 Traceview,我将在下一篇博客内介绍。

(4)拉出 traces 文件

发生ANR时,Android会保存trace信息,在较老的release版本上,设备上只有一个 /data/anr/traces.txt文件;而在新的release版本上,有多个 /data/anr/anr_* 文件。你可以使用 adb 从设备或模拟器上访问这些traces文件:

adb root
adb shell ls /data/anr
adb pull /data/anr/<filename>

如何修复ANR问题

(1)主线程的 Slow code(运行慢的代码)

在你的代码里面定位主线程繁忙时间超过5秒的地方,找一些可疑的用例场景并重现ANR。比如下图显示的 Traceview 的 timeline,主线程繁忙时间超过了5秒:

android ANR讲解-LMLPHP

上图表明耗时操作的代码发生在 onClick 函数里,举例代码如下:

@Override
public void onClick(View view) {
    // 这个任务运行在主线程
    BubbleSort.sort(data);
}

这种情况下,你需要将这段耗时代码移到一个工作线程,Android framework提供了一些类,比如下面的示例代码展现了如何使用 AsyncTask:

@Override
public void onClick(View view) {
   new AsyncTask<Integer[], Integer, Long>() {
       @Override
       protected Long doInBackground(Integer[]... params) {
           BubbleSort.sort(params[0]);// 运行在工作线程
       }
   }.execute(data);
}

Traceview表明大部分代码都运行在工作线程,如下图,主线程能够对用户事件作出响应。

android ANR讲解-LMLPHP

(2)主线程的IO

在主线程上执行IO操作,是主线程上运行缓慢的一个普遍原因,它会造成ANR。如上一节所示,推荐将所有的IO操作都移动到工作线程。IO操作的一些例子是网络和存储,更多信息请参考 执行网络操作 和 保存数据

(3)锁的争夺

在某些场景下,造成ANR的任务并不是直接在主线程执行,如果一个工作线程获得了某个资源的锁,而主线程需要这个资源完成自己的任务,此时可能会发生ANR。

如下图的 Traceview timeline,大部分任务都在工作线程 AsyncTask #2 内执行:

android ANR讲解-LMLPHP

但是,如果正在发生ANR,你应该看下 Android Device Monitor 里面的主线程状态。通常情况下,如果主线程准备更新UI,也响应正常的话,主线程的状态是 Runnable。如果主线程无法恢复执行,然后就会处于 BLOCKED 状态,无法响应事件。在 Android Device Monitor 上显示的状态是 Monitor 或者 Wait,如下表:

android ANR讲解-LMLPHP

下面的 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)
...

Reviewing the trace can help you locate the code that blocks the main thread. The following code is responsible for holding the lock that blocks the main thread in the previous trace:

分析 trace可以帮助你定位阻塞主线程的代码,以下代码就是持有了阻塞主线程的锁:

@Override
public void onClick(View v) {
   // 工作线程获得了 lockedResource 的锁
   new LockTask().execute(data);

   synchronized (lockedResource) {
       // 主线程在这里需要 lockedResource,但是它必须等待 LockTask 使用完成
   }
}

public class LockTask extends AsyncTask<Integer[], Integer, Long> {
   @Override
   protected Long doInBackground(Integer[]... params) {
       synchronized (lockedResource) {
           // 这是一个长时间运行的操作,使得锁持续了一段时间
           BubbleSort.sort(params[0]);
       }
   }
}

另一个例子是主线程正在等待另一个工作线程的结果,如下代码所示。

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();
       }
   }
}

还有一些其他场景可以阻塞主线程,包括使用 Lock、Semaphore、资源池(比如数据库连接池)的线程或其他 mutex 机制。你应该评估你的程序中对一般资源所持有的锁,但是如果你想避免ANR,你应该注意下主线程所需要的那些资源所持有的锁。确保锁被持有最少时间,评估下这个程序首先是否需要锁,如果你使用锁并基于工作线程的处理来决定何时更新UI,使用像 onProgressUpdate() and onPostExecute() 这种机制来实现主线程和工作线程之间的通信。

死锁

当一个线程所需要的资源被另一个线程持有时,它进入了等待状态,而另一个线程也在等待被第一个线程持有的资源,就会发生死锁。如果主线程处于这种情况,很有可能发生ANR。死锁是计算机科学里面研究的比较好的现象,而且你可以使用一些死锁预防算法来避免死锁,更多细节请参考Wikipedia上的 Deadlock and Deadlock prevention algorithms

执行缓慢的广播接收器

应用程序可以对广播消息作出响应,比如启用或禁用飞行模式、网络连接状态的改变,都可以由广播接收器实现。当程序长时间处理广播消息时,会发生ANR。

ANR发生在下列情况:

  1. BroadcastReceiver在相当长的时间内没有执行完它的 onReceive() 方法
  2. BroadcastReceiver调用了 goAsync() 方法,然后在 PendingResult 对象上调用 finish() 失败了

在 BroadcastReceiver 的 onReceive() 方法里面只应该执行短时间的操作,如果你的程序需要处理一个更加复杂的广播消息,你应该将你的任务交给 IntentService 执行。你可以使用像 Traceview 这样的工具来确认你的 Receiver 是否在主线程内执行长时间的操作,比如,下图的 timeline 显示了 broadcast receiver在主线程处理了一个消息将近 100秒:

android ANR讲解-LMLPHP

这个行为可以被 BroadcastReceiver 的 onReceive() 方法内执行长时间操作而引起,比如下面的示例代码:

@Override
public void onReceive(Context context, Intent intent) {
    // 长时间的操作
    BubbleSort.sort(data);
}

像这种情况,推荐将长时间操作的代码移动到 IntentService 去实现,因为它使用了一个工作线程去执行任务。下面的代码展示了如何使用 IntentService 去处理一个长时间操作:

@Override
public void onReceive(Context context, Intent intent) {
    // 现在这个任务运行在工作线程
    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);
   }
}

As a result of using the IntentService, the long-running operation is executed on a worker thread instead of the main thread. Figure 7 shows the work deferred to the worker thread in the Traceview timeline.

使用 IntentService 的结果是,这个长时间的操作不是执行在主线程,而是执行在一个工作线程,如下图所示:

android ANR讲解-LMLPHP

你的广播接收器可以使用 goAsync() 方法向系统发出信号,告诉它需要更多时间去处理消息。然而,你还应该在 PendingResult 对象上调用 finish() 方法。下面这个例子展示了如何调用 finish() 方法让系统去回收广播消息和避免ANR:

final PendingResult pendingResult = goAsync();
new AsyncTask<Integer[], Integer, Long>() {
   @Override
   protected Long doInBackground(Integer[]... params) {
       // 长时间的操作
       BubbleSort.sort(params[0]);
       pendingResult.finish();
   }
}.execute(data);

然而,如果这个广播处于后台,将代码移到另一个线程并使用 goAsync() 并不会修复ANR,ANR超时仍然会生效。

更多关于ANR的信息,请参考 Keeping your app responsive。更多关于线程的信息,请参考 Threading performance

10-05 20:06