ANR

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

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

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

出现以下任何情况时,系统都会针对您的应用触发 ANR:

  • 输入调度超时:如果您的应用在 5 秒内未响应输入事件(例如按键或屏幕触摸)。
  • 执行服务:如果应用声明的服务无法在几秒内完成 Service.onCreateService.onStartCommand/Service.onBind 执行。
  • 未调用 Service.startForeground:如果您的应用使用 Context.startForegroundService 在前台启动新服务,但该服务在 5 秒内未调用 startForeground
  • intent 广播:如果 BroadcastReceiver 在设定的一段时间内没有执行完毕。如果应用有任何前台 activity,此超时期限为 5 秒。
  • JobScheduler 互动:如果 JobService 未在几秒钟内从 JobService.onStartJobJobService.onStopJob 返回,或者如果用户发起的作业启动,而您的应用在调用 JobService.onStartJob 后的几秒内未调用 JobService.setNotification。对于以 Android 13 及更低版本为目标平台的应用,ANR 会保持静默状态,且不会报告给应用。对于以 Android 14 及更高版本为目标平台的应用,ANR 会保持活动状态,并会报告给应用。

如果您的应用遇到 ANR 错误,您可以使用本文档中的指南来诊断和解决问题。

诊断 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 对话框。

重组瓶颈

使用 Android Studio 性能分析器布局检查器来找出重组瓶颈。如需了解详情,请参阅 Jetpack Compose 性能

拉取跟踪信息文件

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。

一个常见的问题是在可组合项中直接运行长时间运行的任务:

@Composable
fun BadList(rawStrings: List<String>) {
    // Math or sorting inside the composable runs on EVERY recomposition pass!
    val heavilyProcessedList = rawStrings
        .filter { it.isNotBlank() }
        .map { it.uppercase().reversed() }
        .map { it.computationallyHeavyFunction() }
.sortedBy { it.length }
    LazyColumn { items(sortedList) { Text(it) } }
}

// Modern Compose-first fix
@Composable
fun GoodList(viewModel: MyViewModel = viewModel()) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

    // UI simply renders state; no heavy processing allowed here
    LazyColumn { items(uiState.sortedData) { Text(it) } }
}

主线程上的 I/O

在主线程上执行 I/O 操作是导致主线程上操作速度缓慢的常见原因,主线程上操作速度缓慢会导致 ANR。在 Compose 中,开发者在尝试派生初始状态时,经常会意外触发磁盘读取(例如 SharedPreferences 或数据库调用)。

在界面层之外执行长时间运行的 I/O 操作。在 ViewModel 中使用 withContext(Dispatchers.IO),或者,更好的是,在数据层中使用 Repository

死锁

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

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

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

使用 Kotlin 和 Compose 时,您可以使用非阻塞协程互斥锁 (Mutex.withLock) 替换原始锁,通过挂起执行上下文(而不是冻结界面线程)来防止线程阻塞。例如:

import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

// Modern non-blocking concurrency state architecture
class SecureDataRepository {
    private val mutex = Mutex()

    suspend fun safeUIAccess() {
        // If locked, the main thread suspends seamlessly, preventing an ANR
        mutex.withLock {
            performSafeOperation()
        }
    }
}

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

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

以下情况下会发生 ANR:

您的应用应只在 BroadcastReceiveronReceive 方法中执行短操作。不过,如果您的应用因广播消息而需要进行更复杂的处理,您应将该任务推迟到 ViewModel(利用 Kotlin 协程、作用域和调度器的强大功能),前提是该任务预计最多需要几秒钟的时间;或者推迟到任何类型的状态容器 (state holder),或者推迟到 WorkManager,前提是该任务预计需要几秒钟以上的时间。

GameActivity

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

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

其他资源

查看内容

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