欢迎参加我们将于 6 月 3 日举行的 #Android11:Beta 版发布会

ANR

如果 Android 应用的界面线程处于阻塞状态的时间过长,会触发“应用无响应”(ANR) 错误。如果应用位于前台,系统会向用户显示一个对话框,如图 1 所示。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 时需要考虑以下几种常见模式:

  1. 应用在主线程上非常缓慢地执行涉及 I/O 的操作。
  2. 应用在主线程上进行长时间的计算。
  3. 主线程在对另一个进程进行同步 binder 调用,而后者需要很长时间才能返回。
  4. 主线程处于阻塞状态,为发生在另一个线程上的长操作等待同步的块。
  5. 主线程在进程中或通过 binder 调用与另一个线程之间发生死锁。主线程不只是在等待长操作执行完毕,而且处于死锁状态。如需更多信息,请参阅维基百科上的死锁

以下方法可帮助您找出是以上哪种原因造成了 ANR。

严格模式

使用 StrictMode 有助于您在开发应用时发现主线程上的意外 I/O 操作。您可以在应用级别或 Activity 级别使用 。

启用后台 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. 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 时间轴中显示了由工作线程处理的工作

图 3. TraceView 时间轴中显示了由工作线程处理的工作

主线程上的 IO

在主线程上执行 IO 操作是导致主线程上操作速度缓慢的常见原因,主线程上操作速度缓慢会导致 ANR。建议将所有 IO 操作移至工作线程,如上一部分所示。

IO 操作示例包括网络和存储操作。如需了解详情,请参阅执行网络操作保存数据

锁争用

在某些情况下,导致 ANR 的工作并不是直接在应用的主线程上执行。如果某工作线程持有对某项资源的锁,而该资源是主线程完成其工作所必需的,这种情况下就可能会发生 ANR。

例如,图 4 显示的 TraceView 时间轴中,大部分工作是在工作线程上执行的。

图 4. TraceView 时间轴中显示了工作线程上正在执行的工作

图 4. TraceView 时间轴中显示了工作线程上正在执行的工作

但如果用户仍然会遇到 ANR,您应该在 Android Device Monitor 中查看主线程的状态。通常情况下,如果主线程已准备好更新界面并且总体上响应速度较快,则处于 RUNNABLE 状态。

但如果主线程无法继续执行,则它处于 BLOCKED 状态,并且无法响应事件。该状态在 Android Device Monitor 中会显示为“Monitor”或“Wait”,如图 5 所示。

图 5. 处于“Monitor”状态的主线程

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

还有一些其他情况会阻塞主线程,包括使用 LockSemaphore 以及资源池(如数据库连接池)或其他互斥(互斥锁)机制的线程。

您应总体上评估应用对资源持有的锁,但如果您想避免 ANR,则应查看对主线程所需资源持有的锁。

请确保将持有锁的时间降到最少,或者最好从一开始就评估应用是否需要持有锁。如果您使用锁来确定何时根据工作线程的处理情况来更新界面,请使用 onProgressUpdate()onPostExecute() 之类的机制在工作线程和主线程之间进行通信。

死锁

线程进入等待状态时会发生死锁,因为所需资源由另一个线程持有,而该线程也在等待第一个线程持有的资源。如果应用的主线程处于这种情况,很可能会发生 ANR。

计算机科学领域对死锁现象进行了充分研究,目前有一些死锁预防算法可用于避免死锁。

如需了解详情,请参阅维基百科上的死锁死锁预防算法

执行速度缓慢的广播接收器

应用可以通过广播接收器响应广播消息,例如启用或停用飞行模式或更改连接状态。如果应用处理广播消息的用时过长,就会发生 ANR。

以下情况下会发生 ANR:

您的应用应只在 BroadcastReceiveronReceive() 方法中执行短操作。不过,如果您的应用因广播消息而需要进行更复杂的处理,则应将该任务推迟到 IntentService

您可以使用 TraceView 等工具来识别广播接收器是否在应用的主线程上执行长时间运行的操作。例如,图 6 显示了某广播接收器的时间轴,该接收器在主线程上处理消息用时大约 100 秒。

图 6. TraceView 时间轴中显示了主线程上的 BroadcastReceiver 工作

图 6. TraceView 时间轴中显示了主线程上的 BroadcastReceiver 工作

如果对 BroadcastReceiveronReceive() 方法执行长时间运行的操作,可能会导致此行为,如以下示例所示:

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 时间轴中显示了在工作线程上处理的广播消息

图 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,请参阅让您的应用随时能迅速响应。要详细了解线程,请参阅线程性能