ANR

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

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

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

ANR 是一个问题,因为负责更新界面的应用主线程无法处理用户输入事件或绘制操作,这会引起用户的不满。如需详细了解应用的主线程,请参阅进程和线程

出现以下任何情况时,系统都会针对您的应用触发 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 发生率:遇到过至少 1 次用户感知的 ANR 的日活跃用户数所占的百分比。目前,我们仅将 Input dispatching timed out 类型的 ANR 视为用户感知。
  • 多次 ANR 发生率:遇到过至少 2 次 ANR 的日活跃用户数所占百分比。

“日活跃用户数”是指在一天内(可能在多次会话中)使用您的应用的唯一身份用户数。如果一位用户在一天中通过多台设备使用您的应用,则系统会针对每台设备统计这一天的活跃用户数。如果多位用户在一天中使用同一台设备,系统会将其统计为一位活跃用户。

“用户感知的 ANR 发生率”是 Android Vitals 核心指标,也就是说,它会影响您的应用在 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 对话框

只有在设备的开发者选项中启用了显示所有 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>

您可以使用设备上的“生成 bug 报告”开发者选项或开发机器上的 adb bugreport 命令,从实体设备获取 bug 报告。如需了解详情,请参阅获取和阅读 bug 报告

解决问题

找出问题后,您可以参考本节中的提示解决常见问题。

主线程上执行速度缓慢的代码

在您的代码中找出应用的主线程忙碌时间超过 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 框架中包含有助于将任务移至工作器线程的类。如需了解详情,请参阅工作器线程

主线程上的 IO

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

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

锁争用

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

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

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

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

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

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

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

图 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)
...

查看跟踪信息有助于找到阻塞主线程的代码。以下代码持有的锁导致了前面的跟踪信息中的主线程阻塞:

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 超时仍然适用。

GameActivity

GameActivity 库可以减少针对使用 C 或 C++ 编写的游戏和应用所做的案例研究中探讨的 ANR 问题。如果您将现有的原生 activity 替换为 GameActivity,则可以减少界面线程阻塞并防止某些 ANR 发生。

如需详细了解 ANR,请参阅让您的应用随时能迅速响应。如需详细了解线程,请参阅线程性能

  • 注意:当 JavaScript 处于关闭状态时,系统会显示链接文字
  • 过多唤醒