ANR

当 Android 应用的 UI 线程被阻塞时间过长时,就会触发“应用无响应”(ANR) 错误。如果应用位于前台,系统会向用户显示一个对话框,如图 1 所示。ANR 对话框会向用户提供强制退出应用的机会。

Figure 1. ANR dialog displayed to the user

图 1. 向用户显示的 ANR 对话框

ANR 是一个问题,因为应用的主线程(负责更新 UI)无法处理用户输入事件或进行绘制,从而给用户带来不便。如需详细了解应用的主线程,请参阅进程和线程

当发生以下任一情况时,您的应用会触发 ANR:

  • 输入分派超时:如果您的应用在 5 秒内未响应输入事件(例如按键或屏幕触摸)。
  • 执行服务:如果您的应用声明的服务无法在几秒钟内完成执行 Service.onCreate()Service.onStartCommand()/Service.onBind()
  • 未调用 Service.startForeground():如果您的应用使用 Context.startForegroundService() 在前台启动新服务,但该服务未在 5 秒内调用 startForeground()
  • 广播 intent:如果 BroadcastReceiver 未在设定的时间内完成执行。如果应用有任何 Activity 位于前台,则此超时时间为 5 秒。
  • JobScheduler 交互:如果 JobService 未在几秒钟内从 JobService.onStartJob()JobService.onStopJob() 返回,或者如果启动了用户发起的作业,并且您的应用在调用 JobService.onStartJob() 后未在几秒钟内调用 JobService.setNotification()。对于面向 Android 13 及以下版本的应用,ANR 是静默的,不会报告给应用。对于面向 Android 14 及以上版本的应用,ANR 是显式的,并会报告给应用。

如果您的应用遇到 ANR,您可以使用本文中的指导来诊断并修复问题。

检测问题

如果您已发布应用,则可以使用 Android vitals 查看应用的 ANR 信息。您可以使用其他工具来检测现场 ANR,但请注意,与 Android vitals 不同,第三方工具无法报告旧版 Android(Android 10 及以下版本)上的 ANR。

Android vitals

Android vitals 可以帮助您监控和改进应用的 ANR 率。Android vitals 衡量多种 ANR 率:

  • ANR 率:每日活跃用户中遇到任何类型 ANR 的百分比。
  • 用户感知的 ANR 率:每日活跃用户中至少遇到一次用户感知的 ANR 的百分比。目前,只有 Input dispatching timed out 类型的 ANR 被认为是用户感知的。
  • 多重 ANR 率:每日活跃用户中至少遇到两次 ANR 的百分比。

“每日活跃用户”是指在一天内在一部设备上(可能多次会话)使用您的应用的唯一用户。如果用户在一天内使用您的应用不止一部设备,则每部设备都会计入当日活跃用户数。如果多位用户在一天内使用同一部设备,则计为一名活跃用户。

用户感知的 ANR 率是核心重要指标,这意味着它会影响您的应用在 Google Play 上的可发现性。它很重要,因为它计算的 ANR 总是在用户与应用交互时发生,造成最大的干扰。

Play 在此指标上定义了两个不良行为阈值

  • 总体不良行为阈值:在所有设备型号上,至少 0.47% 的每日活跃用户遇到用户感知的 ANR。
  • 每设备不良行为阈值:对于单个设备型号,至少 8% 的每日用户遇到用户感知的 ANR。

如果您的应用超出总体不良行为阈值,它在所有设备上的可发现性可能会降低。如果您的应用在某些设备上超出每设备不良行为阈值,它在这些设备上的可发现性可能会降低,并且您的商店详情可能会显示警告。

当您的应用出现过多 ANR 时,Android vitals 可以通过 Play 管理中心向您发出提醒。

如需了解 Google Play 如何收集 Android vitals 数据,请参阅 Play 管理中心文档。

诊断 ANR

诊断 ANR 时,有一些常见的模式需要注意:

  • 应用在主线程上执行涉及 I/O 的慢速操作。
  • 应用在主线程上执行长时间计算。
  • 主线程正在对另一个进程进行同步 Binder 调用,并且该进程需要很长时间才能返回。
  • 主线程因等待另一个线程上发生的长时间操作的同步块而被阻塞。
  • 主线程与另一个线程(无论是您进程中的线程还是通过 Binder 调用)发生死锁。主线程不仅仅是在等待长时间操作完成,而是处于死锁状态。如需了解详情,请参阅维基百科上的死锁

以下技术可以帮助您确定 ANR 的原因。

HealthStats

HealthStats 通过捕获总用户和系统时间、CPU 时间、网络、无线电统计信息、屏幕开/关时间以及唤醒闹钟来提供应用健康状况的指标。这有助于衡量整体 CPU 使用率和电池消耗。

调试

Debug 有助于在开发期间检查 Android 应用,包括跟踪和分配计数,以识别应用中的卡顿和延迟。您还可以使用 Debug 获取运行时和原生内存计数器以及内存指标,这有助于识别特定进程的内存占用。

ApplicationExitInfo

ApplicationExitInfo 在 Android 11(API 级别 30)或更高版本上可用,提供有关应用退出的原因的信息。这包括 ANR、内存不足、应用崩溃、CPU 使用率过高、用户中断、系统中断或运行时权限更改。

严格模式

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

启用后台 ANR 对话框

Android 仅在设备的开发者选项中启用了显示所有 ANR 时,才会针对处理广播消息时间过长的应用显示 ANR 对话框。因此,后台 ANR 对话框并非总是向用户显示,但应用仍可能遇到性能问题。

Traceview

您可以使用 Traceview 在运行应用时跟踪应用,并通过用例识别主线程繁忙的位置。有关如何使用 Traceview 的信息,请参阅使用 Traceview 和 dmtracedump 剖析

拉取跟踪文件

Android 在遇到 ANR 时会存储跟踪信息。在旧版操作系统中,设备上有一个 /data/anr/traces.txt 文件。在更新的操作系统版本中,有多个 /data/anr/anr_* 文件。您可以使用 Android 调试桥 (adb) 作为 root 用户从设备或模拟器访问 ANR 跟踪文件:

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

您可以通过设备上的“捕获 bug 报告”开发者选项或开发机器上的 adb bugreport 命令,从物理设备捕获 bug 报告。如需了解详情,请参阅捕获和读取 bug 报告

解决问题

确定问题后,您可以使用本节中的提示来修复常见问题。

主线程上的慢速代码

找出代码中应用主线程繁忙时间超过 5 秒的位置。查找应用中可疑的用例并尝试重现 ANR。

例如,图 2 显示了一个 Traceview 时间线,其中主线程繁忙时间超过 5 秒。

Figure 2. Traceview timeline showing a busy main
thread

图 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 包含可帮助将任务移至工作线程的类。如需了解详情,请参阅工作线程

主线程上的 I/O

在主线程上执行 I/O 操作是主线程操作缓慢的常见原因,这可能导致 ANR。建议将所有 I/O 操作移至工作线程,如上一节所示。

I/O 操作的一些示例是网络和存储操作。如需了解详情,请参阅执行网络操作保存数据

锁竞争

在某些情况下,导致 ANR 的工作并非直接在应用的主线程上执行。如果工作线程持有一个主线程完成其工作所需的资源的锁,则可能会发生 ANR。

例如,图 3 显示了一个 Traceview 时间线,其中大部分工作在工作线程上执行。

Figure 3. Traceview timeline that shows the work being executed on a worker
thread

图 3. 显示工作在工作线程上执行的 Traceview 时间线

但如果您的用户仍然遇到 ANR,您应该查看 Android Device Monitor 中主线程的状态。通常,如果主线程已准备好更新 UI 并且通常响应迅速,则它处于 RUNNABLE 状态。

但如果主线程无法恢复执行,则它处于 BLOCKED 状态,无法响应事件。该状态在 Android Device Monitor 上显示为 MonitorWait,如图 5 所示。

Figure 4. Main thread in the Monitor
status

图 4. 处于 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]);
       }
   }
}

另一个例子是应用的主线程正在等待工作线程的结果,如以下代码所示。请注意,使用 wait()notify() 在 Kotlin 中并非推荐模式,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> 的线程,以及资源池(例如数据库连接池)或其他互斥 (mutex) 机制。

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

确保锁持有时间最短,或者更好的是,评估应用是否首先需要持有锁。如果您使用锁来根据工作线程的处理结果确定何时更新 UI,请使用 onProgressUpdate()onPostExecute() 等机制在工作线程和主线程之间进行通信。

死锁

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

死锁是计算机科学中一个被充分研究的现象,并且有死锁预防算法可用于避免死锁。

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

慢速广播接收器

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

在以下情况下会发生 ANR:

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

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

Figure 5. Traceview timeline showing the `BroadcastReceiver` work on the main
thread

图 5. 显示 BroadcastReceiver 在主线程上工作的 Traceview 时间线

这种行为可能是由在 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 时间线中推迟到工作线程的工作。

Figure 6. Traceview timeline showing the broadcast message processed on a
worker thread

图 6. 显示广播消息在工作线程上处理的 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 超时仍然适用。

GameActivity

GameActivity 库在以 C 或 C++ 编写的游戏和应用的案例研究中减少了 ANR。如果您将现有的原生 Activity 替换为 GameActivity,则可以减少 UI 线程阻塞并防止某些 ANR 的发生。

如需了解有关 ANR 的更多信息,请参阅保持应用响应迅速。如需了解有关线程的更多信息,请参阅线程性能

  • 注意:禁用 JavaScript 时显示链接文本
  • 唤醒过多