诊断和修复 ANR

当 Android 应用的用户界面 (UI) 线程阻塞时间过长时,系统会发送“应用程序无响应” (ANR) 错误。此页面介绍了不同类型的 ANR、如何诊断它们以及修复建议。所有列出的默认超时时间范围均适用于 AOSP 和 Pixel 设备;这些时间可能因 OEM 而异。

请记住,在确定 ANR 的原因时,区分系统问题和应用问题很有帮助。

当系统处于不良状态时,以下问题会导致 ANR

  • 系统服务器中的瞬态问题通常会导致 Binder 调用变慢。
  • 系统服务器问题和设备负载过高会导致应用线程无法调度。

如果可用,区分系统问题和应用问题的一个好方法是使用 Perfetto 追踪

  • 查看 Perfetto 中的线程状态跟踪,以查看应用的主线程是否已调度,以及它是否正在运行或可运行。
  • 查看 system_server 线程是否存在诸如锁争用之类的问题。
  • 对于缓慢的 Binder 调用,请查看回复线程(如果存在),以了解其缓慢的原因。

输入分发超时

当应用的主线程未能在规定时间内响应输入事件(例如滑动或按键)时,就会发生输入分发 ANR。由于应用在输入分发超时发生时位于前台,因此它们几乎始终对用户可见,并且非常重要,需要缓解。

默认超时时间:5 秒。

输入分发 ANR 通常是由主线程上的问题引起的。如果主线程被阻塞等待获取锁,则持有者线程也可能参与其中。

为了避免输入分发 ANR,请遵循以下最佳实践

  • 不要在主线程上执行阻塞或长时间运行的操作。考虑使用 StrictMode 来捕获主线程上的意外活动。
  • 最大程度减少主线程与其他线程之间的锁争用。
  • 最大程度减少主线程上的非 UI 工作,例如处理广播或运行服务时。

常见原因

以下是输入分发 ANR 的一些常见原因和建议的修复方法。

原因 发生了什么 建议的修复方法
缓慢的 Binder 调用 主线程进行长时间的同步 Binder 调用。 将调用移出主线程,或者如果您拥有 API,则尝试优化调用。
许多连续的 Binder 调用 主线程进行许多连续的同步 Binder 调用。 不要在紧密循环中执行 Binder 调用。
阻塞 I/O 主线程进行阻塞 I/O 调用,例如数据库或网络访问。 将所有阻塞 IO 移出主线程。
锁争用 主线程被阻塞等待获取锁。 减少主线程与其他线程之间的锁争用。优化其他线程中的缓慢代码。
昂贵的帧 在单个帧中渲染过多内容,导致严重卡顿。 减少渲染帧的工作量。不要使用 n2 算法。对于滚动或分页之类的事情,使用高效的组件,例如 Jetpack 的 分页库
被其他组件阻塞 其他组件(例如广播接收器)正在运行并阻塞主线程。 尽可能将非 UI 工作移出主线程。在不同的线程上运行广播接收器。
GPU 冻结 GPU 冻结是导致渲染被阻塞并因此导致输入分发 ANR 的系统或硬件问题。 不幸的是,应用端通常没有任何修复方法。如果可能,请联系硬件团队进行故障排除。

如何调试

从查看 Google Play 控制台Firebase Crashlytics 中的 ANR 集群签名开始调试。该集群通常包含疑似导致 ANR 的顶级帧。

以下流程图显示了如何确定输入超时分发 ANR 的原因。

图 1. 如何调试输入分发 ANR。

Play vitals 可以检测并帮助调试其中一些常见的 ANR 原因。例如,如果 vitals 检测到 ANR 是由于锁争用而发生的,则它可以在 ANR 的“见解”部分总结问题并推荐修复方法。

图 2. Play vitals ANR 检测。

无焦点窗口

虽然像触摸这样的事件是根据命中测试直接发送到相关窗口的,但像按键这样的事件需要一个目标。此目标称为焦点窗口。每个显示器只有一个焦点窗口,它通常是用户当前正在与之交互的窗口。如果找不到焦点窗口,则输入会引发无焦点窗口 ANR。无焦点窗口 ANR 是一种输入分发 ANR。

默认超时时间:5 秒。

常见原因

无焦点窗口 ANR 通常是由以下任一问题引起的

  • 应用正在执行过多的工作并且绘制第一帧的速度太慢。
  • 主窗口不可聚焦。如果窗口使用 FLAG_NOT_FOCUSABLE 标记,则用户无法向其发送键或按钮事件。

Kotlin

override fun onCreate(savedInstanceState: Bundle) {
  super.onCreate(savedInstanceState)
  setContentView(R.layout.activity_main)
  window.addFlags(WindowManager.LayoutParams.FLAG_FLAG_NOT_FOCUSABLE)
}

Java

@Override
protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
}

广播接收器超时

当广播接收器未能在规定时间内处理广播时,就会发生广播接收器 ANR。对于同步接收器或未调用 goAsync() 的接收器,超时意味着 onReceive() 未能在规定时间内完成。对于异步接收器或调用 goAsync() 的接收器,超时意味着 PendingResult.finish() 未能在规定时间内被调用。

广播接收器 ANR 通常发生在以下线程中

  • 主线程,如果问题是应用启动缓慢。
  • 运行广播接收器的线程,如果问题是 onReceive() 代码缓慢。
  • 广播工作线程,如果问题是 goAsync() 广播代码缓慢。

为了避免广播接收器 ANR,请遵循以下最佳实践

  • 确保应用启动速度快,因为如果启动应用来处理广播,则它会被计入 ANR 超时时间。
  • 如果使用了 goAsync(),请确保快速调用 PendingResult.finish()。这受与同步广播接收器相同的 ANR 超时限制。
  • 如果使用了 goAsync(),请确保工作线程未与其他长时间运行或阻塞操作共享。
  • 考虑使用 registerReceiver() 在非主线程中运行广播接收器,以避免阻塞主线程中运行的 UI 代码。

超时时间

广播接收超时时间取决于是否设置了前台 Intent 标志以及平台版本。

Intent 类型 Android 13 及更低版本 Android 14 及更高版本

前台优先级 Intent

(已设置 FLAG_RECEIVER_FOREGROUND)

10 秒

10-20 秒,具体取决于进程是否处于 CPU 饥饿状态

后台优先级 Intent

(未设置 FLAG_RECEIVER_FOREGROUND)

60 秒

60-120 秒,具体取决于进程是否处于 CPU 饥饿状态

要判断是否设置了 FLAG_RECEIVER_FOREGROUND 标志,请在 ANR 主题中查找“flg=”并检查是否存在 0x10000000。如果设置了此位,则 Intent 已设置 FLAG_RECEIVER_FOREGROUND,因此超时时间较短。

具有短广播超时(10-20 秒)的 ANR 主题示例

Broadcast of Intent { act=android.inent.action.SCREEN_ON flg=0x50200010 }

具有长广播超时(60-120 秒)的 ANR 主题示例

Broadcast of Intent { act=android.intent.action.TIME_SET flg=0x25200010 }

广播时间如何测量

广播持续时间测量从 system_server 向应用分发广播时开始,并在应用完成处理广播时结束。如果应用进程尚未运行,则它还需要在 ANR 超时时间内进行冷启动。因此,应用启动缓慢 会导致广播接收器 ANR。

下图说明了广播接收器 ANR 时间线与某些应用进程保持一致。

图 3. 广播接收器 ANR 时间线。

当接收器完成处理广播时,ANR 超时测量结束:这何时发生的确切时间取决于它是同步接收器还是异步接收器。

  • 对于同步接收器,测量在 onReceive() 返回时停止。
  • 对于异步接收器,测量在调用 PendingResult.finish() 时停止。
图 4. 同步和异步接收器的 ANR 超时测量端点。

常见原因

以下是广播接收器 ANR 的一些常见原因和建议的修复方法。

原因 适用于 发生了什么 建议的修复方法
应用启动缓慢 所有接收器 应用花费的时间过长才能进行冷启动。 优化应用启动缓慢的问题。
onReceive() 未调度 所有接收器 广播接收器线程忙于执行其他工作,无法启动 onReceive() 方法。 不要在接收器线程上执行长时间运行的任务(或将接收器移至专用线程)。
onReceive() 缓慢 所有接收器,但主要是同步接收器 onReceive() 方法已启动,但被阻塞或缓慢,因此未能在规定时间内完成。 优化缓慢的接收器代码。
异步接收器任务未调度 goAsync() 接收器 onReceive() 方法尝试在被阻塞的工作线程池上执行工作,因此工作从未开始。 优化缓慢或阻塞的调用,或为广播工作程序与其他长时间运行的任务使用不同的线程。
工作程序缓慢或被阻塞 goAsync() 接收器 在处理广播时,工作线程池中的某个地方存在阻塞或缓慢的操作。因此,PendingResult.finish 未能在规定时间内被调用。 优化缓慢的 async 接收器代码。
忘记调用 PendingResult.finish goAsync() 接收器 代码路径中缺少对 finish() 的调用。 确保始终调用 finish()

如何调试

根据集群签名和 ANR 报告,您可以找到接收器运行的线程,然后找到缺少或运行缓慢的特定代码。

以下流程图显示了如何确定广播接收器 ANR 的原因。

图 5. 如何调试广播接收器 ANR。

查找接收器代码

Google Play Console 在 ANR 签名中显示接收器类和广播意图。查找以下内容

  • cmp=<receiver class>
  • act=<broadcast_intent>

这是一个广播接收器 ANR 签名的示例

com.example.app.MyClass.myMethod
Broadcast of Intent { act=android.accounts.LOGIN_ACCOUNTS_CHANGED
cmp=com.example.app/com.example.app.MyAccountReceiver }

查找运行 onReceive() 方法的线程

如果您使用Context.registerReceiver 指定自定义处理程序,则它是运行此处理程序的线程。否则,它是主线程。

示例:异步接收器任务未调度

本节将逐步介绍如何调试广播接收器 ANR 的示例。

假设 ANR 签名如下所示

com.example.app.MyClass.myMethod
Broadcast of Intent {
act=android.accounts.LOG_ACCOUNTS_CHANGED cmp=com.example.app/com.example.app.MyReceiver }

根据签名,广播意图似乎是android.accounts.LOG_ACCOUNTS_CHANGED,接收器类是com.example.app.MyReceiver

从接收器代码中,您可以确定线程池“BG Thread [0,1,2,3]”执行处理此广播的主要工作。查看堆栈转储,您可以看到所有四个后台 (BG) 线程具有相同的模式:它们运行一个阻塞调用getDataSync。由于所有 BG 线程都很繁忙,因此广播无法及时处理,从而导致 ANR。

BG Thread #0 (tid=26) Waiting

at jdk.internal.misc.Unsafe.park(Native method:0)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:211)
at com.google.common.util.concurrent.AbstractFuture.get(AbstractFuture:563)
at com.google.common.util.concurrent.ForwardingFuture.get(ForwardingFuture:68)
at com.example.app.getDataSync(<MyClass>:152)

...

at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644)
at com.google.android.libraries.concurrent.AndroidExecutorsModule.lambda$withStrictMode$5(AndroidExecutorsModule:451)
at com.google.android.libraries.concurrent.AndroidExecutorsModule$$ExternalSyntheticLambda8.run(AndroidExecutorsModule:1)
at java.lang.Thread.run(Thread.java:1012)
at com.google.android.libraries.concurrent.ManagedPriorityThread.run(ManagedPriorityThread:34)

有几种方法可以解决此问题

  • 找出getDataSync 速度慢的原因并进行优化。
  • 不要在所有四个 BG 线程上运行getDataSync
  • 更一般地说,确保 BG 线程池没有被长时间运行的操作饱和。
  • goAsync 工作器任务使用专用的线程池。
  • 使用无界线程池代替有界 BG 线程池

示例:应用启动缓慢

应用启动缓慢会导致多种类型的 ANR,尤其是广播接收器和执行服务 ANR。如果您在主线程堆栈中看到ActivityThread.handleBindApplication,则 ANR 的原因可能是应用启动缓慢。

执行服务超时

当应用的主线程未及时启动服务时,就会发生执行服务 ANR。具体来说,服务在超时时间内未完成执行onCreate()onStartCommand()onBind()

默认超时时间:前台服务为 20 秒;后台服务为 200 秒。ANR 超时时间包括应用冷启动(如有必要)以及对onCreate()onBind()onStartCommand() 的调用。

为了避免执行服务 ANR,请遵循以下常规最佳实践

  • 确保应用启动速度快,因为如果启动应用以运行服务组件,则会在 ANR 超时时间内计算在内。
  • 确保服务的onCreate()onStartCommand()onBind() 方法速度快。
  • 避免从其他组件在主线程上运行任何缓慢或阻塞操作;这些操作可能会阻止服务快速启动。

常见原因

下表列出了执行服务 ANR 的常见原因和建议的修复方法。

原因 什么 建议的修复方法
应用启动缓慢 应用花费太长时间执行冷启动。 优化应用启动缓慢的问题。
缓慢的onCreate()onStartCommand()onBind() 服务组件的onCreate()onStartCommand()onBind() 方法在主线程上执行花费的时间过长。 优化缓慢的代码。尽可能将缓慢的操作从关键路径中移开。
未调度(在onStart() 之前主线程被阻塞) 在服务可以启动之前,应用的主线程被另一个组件阻塞。 将其他组件的工作移出主线程。优化其他组件的阻塞代码。

如何调试

从 Google Play Console 或 Firebase Crashlytics 中的集群签名和 ANR 报告中,您通常可以根据主线程正在执行的操作来确定 ANR 的原因。

以下流程图描述了如何调试执行服务 ANR。

图 6. 如何调试执行服务 ANR。

如果您已确定执行服务 ANR 可操作,请按照以下步骤帮助解决此问题

  1. 在 ANR 签名中查找服务组件类。在 Google Play Console 中,服务组件类显示在 ANR 签名中。在以下 ANR 详细信息示例中,它是com.example.app/MyService

    com.google.common.util.concurrent.Uninterruptibles.awaitUninterruptibly
    Executing service com.example.app/com.example.app.MyService
    
  2. 通过检查主线程中的以下重要函数调用,确定缓慢或阻塞操作是应用启动、服务组件还是其他地方的一部分。

    主线程堆栈中的函数调用 含义
    android.app.ActivityThread.handleBindApplication 应用正在启动,因此 ANR 是由应用启动缓慢引起的。

    <ServiceClass>.onCreate()

    [...]

    android.app.ActivityThread.handleCreateService

    正在创建服务,因此 ANR 可能是由onCreate() 代码缓慢引起的。

    <ServiceClass>.onBind()

    [...]

    android.app.ActivityThread.handleBindService

    正在绑定服务,因此 ANR 可能是由onBind() 代码缓慢引起的。

    <ServiceClass>.onStartCommand()

    [...]

    android.app.ActivityThread.handleServiceArgs

    正在启动服务,因此 ANR 可能是由onStartCommand() 代码缓慢引起的。

    例如,如果MyService 类中的onStartCommand() 方法速度很慢,则主线程将如下所示

    at com.example.app.MyService.onStartCommand(FooService.java:25)
    at android.app.ActivityThread.handleServiceArgs(ActivityThread.java:4820)
    at android.app.ActivityThread.-$$Nest$mhandleServiceArgs(unavailable:0)
    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2289)
    at android.os.Handler.dispatchMessage(Handler.java:106)
    at android.os.Looper.loopOnce(Looper.java:205)
    at android.os.Looper.loop(Looper.java:294)
    at android.app.ActivityThread.main(ActivityThread.java:8176)
    at java.lang.reflect.Method.invoke(Native method:0)
    

    如果您看不到任何重要函数调用,则还有其他几种可能性

    • 服务正在运行或关闭,这意味着堆栈获取过晚。在这种情况下,您可以忽略 ANR 作为误报。
    • 其他应用组件正在运行,例如广播接收器。在这种情况下,主线程可能在此组件中被阻塞,从而阻止服务启动。
  3. 如果您确实看到了关键函数调用,并且可以大致确定 ANR 发生的位置,请检查主线程的其他堆栈以查找缓慢的操作并对其进行优化或将其移出关键路径。

有关服务的更多信息,请参阅以下页面

内容提供程序无响应

当远程内容提供程序花费的时间超过超时时间才能响应查询并被杀死时,就会发生内容提供程序 ANR。

默认超时时间:由内容提供程序使用ContentProviderClient.setDetectNotResponding 指定。ANR 超时时间包括远程内容提供程序查询运行的总时间,如果远程应用尚未运行,则包括冷启动远程应用。

为了避免内容提供程序 ANR,请遵循以下最佳实践

  • 确保应用启动速度快,因为如果启动应用以运行内容提供程序,则会在 ANR 超时时间内计算在内。
  • 确保内容提供程序查询速度快。
  • 不要执行大量可能阻塞所有应用绑定程序线程的并发阻塞绑定程序调用。

常见原因

下表列出了内容提供程序 ANR 的常见原因和建议的修复方法。

原因 发生了什么 信号 建议的修复方法
内容提供程序查询缓慢 内容提供程序花费的时间过长才能执行或被阻塞。 绑定程序线程中存在android.content.ContentProvider$Transport.query 帧。 优化内容提供程序查询。找出阻塞绑定程序线程的原因。
应用启动缓慢 内容提供程序的应用花费的时间过长才能启动。 主线程中存在ActivityThread.handleBindApplication 帧。 优化应用启动。
绑定程序线程耗尽——所有绑定程序线程都很繁忙 所有绑定程序线程都忙于为其他同步请求提供服务,因此无法运行内容提供程序绑定程序调用。 应用未启动,所有绑定程序线程都很繁忙,并且内容提供程序未运行。 减少绑定程序线程上的负载。也就是说,减少同步传出的绑定程序调用次数,或者在处理传入调用时减少工作量。

如何调试

要使用 Google Play Console 或 Firebase Crashlytics 中的集群签名和 ANR 报告调试内容提供程序 ANR,请查看主线程和绑定程序线程正在执行的操作。

以下流程图描述了如何调试内容提供程序 ANR

图 7. 如何调试内容提供程序 ANR。

以下代码片段显示了绑定程序线程在由于内容提供程序查询缓慢而被阻塞时的样子。在这种情况下,内容提供程序查询在打开数据库时等待锁。

binder:11300_2 (tid=13) Blocked

Waiting for osm (0x01ab5df9) held by at com.google.common.base.Suppliers$NonSerializableMemoizingSupplier.get(Suppliers:182)
at com.example.app.MyClass.blockingGetOpenDatabase(FooClass:171)
[...]
at com.example.app.MyContentProvider.query(MyContentProvider.java:915)
at android.content.ContentProvider$Transport.query(ContentProvider.java:292)
at android.content.ContentProviderNative.onTransact(ContentProviderNative.java:107)
at android.os.Binder.execTransactInternal(Binder.java:1339)
at android.os.Binder.execTransact(Binder.java:1275)

以下代码片段显示了主线程在由于应用启动缓慢而被阻塞时的样子。在这种情况下,应用启动缓慢是由于 Dagger 初始化期间的锁争用引起的。

main (tid=1) Blocked

[...]
at dagger.internal.DoubleCheck.get(DoubleCheck:51)
- locked 0x0e33cd2c (a qsn)at dagger.internal.SetFactory.get(SetFactory:126)
at com.myapp.Bar_Factory.get(Bar_Factory:38)
[...]
at com.example.app.MyApplication.onCreate(DocsApplication:203)
at android.app.Instrumentation.callApplicationOnCreate(Instrumentation.java:1316)
at android.app.ActivityThread.handleBindApplication(ActivityThread.java:6991)
at android.app.ActivityThread.-$$Nest$mhandleBindApplication(unavailable:0)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2235)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loopOnce(Looper.java:205)
at android.os.Looper.loop(Looper.java:294)
at android.app.ActivityThread.main(ActivityThread.java:8170)
at java.lang.reflect.Method.invoke(Native method:0)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971)

作业响应缓慢

当应用花费的时间过长才能响应JobService.onStartJob()JobService.onStopJob(),或者花费的时间过长才能使用JobService.setNotification() 提供通知时,就会发生作业响应缓慢 ANR。这表明应用的主线程被阻塞执行其他操作。

如果是JobService.onStartJob()JobService.onStopJob() 的问题,请检查主线程上发生的情况。如果是JobService.setNotification() 的问题,请确保尽快调用它。在提供通知之前,不要执行大量工作。

神秘 ANR

有时不清楚 ANR 发生的原因,或者集群签名和 ANR 报告中提供的调试信息不足。在这些情况下,您仍然可以采取一些步骤来确定 ANR 是否可操作。

消息队列空闲或 nativePollOnce

如果您在堆栈中看到帧android.os.MessageQueue.nativePollOnce,则通常表示怀疑无响应的线程实际上处于空闲状态并等待循环程序消息。在 Google Play Console 中,ANR 详细信息如下所示

Native method - android.os.MessageQueue.nativePollOnce
Executing service com.example.app/com.example.app.MyService

例如,如果主线程处于空闲状态,则堆栈将如下所示

"main" tid=1 NativeMain threadIdle

#00  pc 0x00000000000d8b38  /apex/com.android.runtime/lib64/bionic/libc.so (__epoll_pwait+8)
#01  pc 0x0000000000019d88  /system/lib64/libutils.so (android::Looper::pollInner(int)+184)
#02  pc 0x0000000000019c68  /system/lib64/libutils.so (android::Looper::pollOnce(int, int*, int*, void**)+112)
#03  pc 0x000000000011409c  /system/lib64/libandroid_runtime.so (android::android_os_MessageQueue_nativePollOnce(_JNIEnv*, _jobject*, long, int)+44)
at android.os.MessageQueue.nativePollOnce (Native method)
at android.os.MessageQueue.next (MessageQueue.java:339)  at android.os.Looper.loop (Looper.java:208)
at android.app.ActivityThread.main (ActivityThread.java:8192)
at java.lang.reflect.Method.invoke (Native method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:626)
at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1015)

怀疑无响应的线程处于空闲状态的原因有多种

  • 堆栈转储延迟。线程在 ANR 触发和转储堆栈之间的一小段时间内恢复。Android 13 上 Pixel 的延迟约为 100 毫秒,但可能超过 1 秒。Android 14 上 Pixel 的延迟通常小于 10 毫秒。
  • 线程错误归因。用于构建 ANR 签名的线程不是导致 ANR 的实际无响应线程。在这种情况下,请尝试确定 ANR 是否属于以下类型之一
  • 系统范围的问题。由于系统负载过重或系统服务器中的问题,进程未被调度。

无堆栈帧

一些 ANR 报告不包含 ANR 的堆栈信息,这意味着在生成 ANR 报告时堆栈转储失败。缺少堆栈帧可能有几个原因。

  • 获取堆栈信息需要花费太长时间并超时。
  • 在获取堆栈信息之前,进程崩溃或被终止。
[...]

--- CriticalEventLog ---
capacity: 20
timestamp_ms: 1666030897753
window_ms: 300000

libdebuggerd_client: failed to read status response from tombstoned: timeout reached?

----- Waiting Channels: pid 7068 at 2022-10-18 02:21:37.<US_SOCIAL_SECURITY_NUMBER>+0800 -----

[...]

没有堆栈帧的 ANR 无法从集群签名或 ANR 报告中进行操作。要进行调试,请查看应用的其他集群,因为如果问题足够严重,通常会在其中存在堆栈帧的其他集群中出现。另一种选择是查看Perfetto 追踪

已知问题

在您的应用进程中保留一个计时器,以便在 ANR 触发之前完成广播处理,可能无法正常工作,因为系统监控 ANR 的方式是异步的。