诊断并修复 ANR

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

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

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

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

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

  • 通过查看 Perfetto 中的线程状态轨迹,了解应用的 main 线程是否被调度,判断其是否正在运行或可运行。
  • 查看 system_server 线程,查找锁竞争等问题。
  • 对于缓慢的 Binder 调用,如果有回复线程,请查看该线程以了解其缓慢的原因。

输入分发超时

输入分发 ANR 发生在应用的主线程未能及时响应输入事件(例如滑动或按键)时。由于输入分发超时发生时应用处于前台,因此它们几乎总是对用户可见,并且非常重要加以缓解。

默认超时时间:5 秒。

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

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

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

常见原因

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

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

如何调试

通过查看 Google Play ConsoleFirebase 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 签名中显示接收器类和广播 Intent。查找以下内容:

  • cmp=<接收器类>
  • act=<广播_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 }

根据签名,广播 intent 似乎是 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 超时。
  • 确保内容提供器查询速度快。
  • 不要执行大量可能阻塞应用所有 Binder 线程的并发阻塞 Binder 调用。

常见原因

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

原因 发生情况 征兆 建议修复
内容提供器查询缓慢 内容提供器执行时间过长或被阻塞。 android.content.ContentProvider$Transport.query 帧位于 Binder 线程中。 优化内容提供器查询。找出是什么阻塞了 Binder 线程。
应用启动缓慢 内容提供器应用启动时间过长。 ActivityThread.handleBindApplication 帧位于主线程中。 优化应用启动。
Binder 线程耗尽——所有 Binder 线程都忙碌 所有 Binder 线程都忙于处理其他同步请求,因此内容提供器 Binder 调用无法运行。 应用未启动,所有 Binder 线程都忙碌,并且内容提供器未运行。 减轻 Binder 线程上的负载。也就是说,减少同步传出 Binder 调用,或在处理传入调用时减少工作量。

如何调试

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

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

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

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

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)

作业响应缓慢

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

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

神秘 ANR

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

消息队列空闲或 nativePollOnce

如果您在堆栈中看到 android.os.MessageQueue.nativePollOnce 帧,这通常表示可疑的无响应线程实际上处于空闲状态,正在等待 looper 消息。在 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 的方式是异步的。