当Android应用的UI线程阻塞时间过长时,会触发“应用程序无响应”(ANR)错误。如果应用位于前台,系统会向用户显示一个对话框,如图1所示。ANR对话框允许用户强制退出应用。
ANR是一个问题,因为负责更新UI的应用主线程无法处理用户输入事件或绘图,从而导致用户感到沮丧。有关应用主线程的更多信息,请参见进程和线程。
当发生以下任一情况时,将为您的应用触发ANR:
- 输入调度超时:如果您的应用在5秒内未响应输入事件(例如按键或屏幕触摸)。
- 执行服务:如果您的应用声明的服务无法在几秒钟内完成执行
Service.onCreate()
和Service.onStartCommand()
/Service.onBind()
。 - 未调用Service.startForeground():如果您的应用使用
Context.startForegroundService()
在前台启动新服务,但该服务随后未在5秒钟内调用startForeground()
。 - 广播意图:如果
BroadcastReceiver
未在设定的时间内完成执行。如果应用在前台有任何活动,则此超时时间为5秒。 - JobScheduler交互:如果
JobService
未在几秒钟内从JobService.onStartJob()
或JobService.onStopJob()
返回,或者如果用户启动的作业启动,并且您的应用未在调用JobService.onStartJob()
后几秒钟内调用JobService.setNotification()
。对于目标为Android 13及以下版本的应用,ANR是静默的,不会向应用报告。对于目标为Android 14及以上版本的应用,ANR是显式的,会向应用报告。
如果您的应用遇到ANR,您可以使用本文中的指导来诊断和解决问题。
检测问题
如果您已发布您的应用,您可以使用Android Vitals查看有关应用ANR的信息。您可以使用其他工具来检测现场的ANR,但请注意,与Android Vitals不同,3P工具无法报告旧版Android(Android 10及以下版本)上的ANR。
Android Vitals
Android Vitals可以帮助您监控和改善应用的ANR率。Android Vitals测量几种ANR率:
- ANR率:每天活跃用户中遇到任何类型ANR的百分比。
- 用户感知的ANR率:每天活跃用户中至少遇到一次用户感知的ANR的百分比。目前,只有
Input dispatching timed out
类型的ANR被视为用户感知的ANR。 - 多次ANR率:每天活跃用户中至少遇到两次ANR的百分比。
每日活跃用户是指在一天内在一个设备上使用您的应用的唯一用户,可能跨多个会话。如果用户在同一天在一个以上的设备上使用您的应用,则每个设备都会计入当天的活跃用户数量。如果多个用户在同一天使用同一设备,则计为一个活跃用户。
用户感知的ANR率是一个核心指标,这意味着它会影响您的应用在Google Play上的可发现性。它很重要,因为它计数的ANR总是发生在用户使用应用时,造成最大的干扰。
Play已为此指标定义了两个不良行为阈值:
- 整体不良行为阈值:在所有设备型号中,至少有0.47%的每日活跃用户遇到用户感知的ANR。
- 每个设备的不良行为阈值:对于单个设备型号,至少有8%的每日用户遇到用户感知的ANR。
如果您的应用超过整体不良行为阈值,则其在所有设备上的可发现性可能会降低。如果您的应用在某些设备上超过每个设备的不良行为阈值,则其在这些设备上的可发现性可能会降低,并且您的商店列表中可能会显示警告。
Android Vitals可以通过Play控制台提醒您应用何时出现过多的ANR。
有关Google Play如何收集Android Vitals数据的更多信息,请参阅Play控制台文档。
诊断ANR
诊断ANR时,有一些常见的模式需要查找:
- 应用正在执行涉及在主线程上进行I/O的缓慢操作。
- 应用正在主线程上进行长时间计算。
- 主线程正在进行一次同步 Binder 调用到另一个进程,而另一个进程正在花费很长时间才能返回。
- 主线程正在阻塞,等待另一个线程上发生的长时间操作的同步块。
- 主线程与另一个线程死锁,该线程可能在您的进程中,也可能通过 Binder 调用发生。主线程不仅仅是在等待一个长时间操作完成,而是一个死锁的情况。更多信息,请参见维基百科上的死锁。
以下技术可以帮助您确定 ANR 的原因。
HealthStats
HealthStats
通过捕获总用户时间和系统时间、CPU 时间、网络、无线电统计信息、屏幕开启/关闭时间以及唤醒闹钟来提供有关应用程序健康状况的指标。这有助于衡量整体 CPU 使用率和电池消耗。
Debug
Debug
有助于在开发过程中检查 Android 应用程序,包括跟踪和分配计数,以识别应用程序中的卡顿和延迟。您还可以使用 Debug
获取运行时和原生内存计数器以及内存指标,这有助于识别特定进程的内存占用。
ApplicationExitInfo
ApplicationExitInfo
在 Android 11(API 级别 30)或更高版本上可用,并提供有关应用程序退出原因的信息。这包括 ANR、内存不足、应用程序崩溃、CPU 使用率过高、用户中断、系统中断或运行时权限更改。
严格模式
使用StrictMode
可以帮助您在开发应用程序时查找主线程上的意外 I/O 操作。您可以在应用程序或活动级别使用StrictMode
。
启用后台 ANR 对话框
仅当设备的开发者选项中启用了显示所有 ANR 时,Android 才会为处理广播消息花费太长时间的应用程序显示 ANR 对话框。因此,后台 ANR 对话框并不总是显示给用户,但应用程序仍然可能遇到性能问题。
Traceview
您可以使用 Traceview 在执行用例时获取正在运行的应用程序的跟踪,并识别主线程繁忙的位置。有关如何使用 Traceview 的信息,请参见使用 Traceview 和 dmtracedump 进行性能分析。
提取 traces 文件
Android 在遇到 ANR 时会存储跟踪信息。在较旧的 OS 版本中,设备上只有一个/data/anr/traces.txt
文件。在较新的 OS 版本中,有多个/data/anr/anr_*
文件。您可以使用以 root 权限运行的Android 调试桥 (adb)从设备或模拟器访问 ANR 跟踪。
adb root
adb shell ls /data/anr
adb pull /data/anr/<filename>
您可以使用设备上的“获取错误报告”开发者选项,或开发机器上的 adb bugreport 命令,从物理设备捕获错误报告。更多信息,请参见捕获和读取错误报告。
修复问题
确定问题后,您可以使用本节中的提示来修复常见问题。
主线程上的缓慢代码
确定应用程序的主线程在超过 5 秒内繁忙的代码位置。查找应用程序中可疑的用例,并尝试重现 ANR。
例如,图 2 显示了一个 Traceview 时间线,其中主线程繁忙超过 5 秒。
图 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。
例如,图 3 显示了一个 Traceview 时间线,其中大部分工作是在工作线程上执行的。
图 3. 显示工作在工作线程上执行的 Traceview 时间线
但是,如果您的用户仍然遇到 ANR,您应该查看 Android 设备监视器中主线程的状态。通常,如果主线程已准备好更新 UI 并通常具有响应性,则它处于RUNNABLE
状态。
但是,如果主线程无法恢复执行,则它处于BLOCKED
状态,并且无法响应事件。该状态在 Android 设备监视器上显示为“监视器”或“等待”,如图 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(); } } }
还有一些其他情况可能会阻塞主线程,包括使用Lock
、Semaphore
以及资源池(例如数据库连接池)或其他互斥(mutex)机制的线程。
您应该评估应用程序通常持有的资源锁,但是如果您想避免 ANR,那么您应该查看为主线程所需的资源持有的锁。
确保锁保持的时间最短,或者更好的是,评估应用程序是否首先需要该锁。如果您使用锁来确定何时根据工作线程的处理来更新 UI,请使用诸如onProgressUpdate()
和onPostExecute()
之类的机制来在工作线程和主线程之间进行通信。
死锁
当一个线程进入等待状态,因为所需的资源被另一个线程持有,而另一个线程也在等待第一个线程持有的资源时,就会发生死锁。如果应用程序的主线程处于这种情况,则很可能会发生 ANR。
死锁是计算机科学中一个经过充分研究的现象,您可以使用死锁预防算法来避免死锁。
缓慢的广播接收器
应用程序可以通过广播接收器响应广播消息,例如启用或禁用飞行模式或连接状态更改。当应用程序花费太长时间来处理广播消息时,就会发生 ANR。
在以下情况下会发生 ANR
- 广播接收器在相当长的时间内尚未完成其
onReceive()
方法的执行。 - 广播接收器调用
goAsync()
并且未能对PendingResult
对象调用finish()
。
您的应用程序应该只在BroadcastReceiver
的onReceive()
方法中执行简短操作。但是,如果您的应用程序需要由于广播消息而进行更复杂的处理,则应将任务推迟到IntentService
。
您可以使用 Traceview 等工具来确定您的广播接收器是否在应用程序的主线程上执行长时间运行的操作。例如,图 6 显示了在主线程上处理消息大约 100 秒的广播接收器的时间线。
此行为可能是由于在BroadcastReceiver
的onReceive()
方法中执行长时间运行的操作引起的,如下例所示
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 时间线中推迟到工作线程的工作。
您的广播接收器可以使用goAsync()
来告知系统它需要更多时间来处理消息。但是,您应该在PendingResult
对象上调用finish()
。以下示例演示了如何调用finish()以让系统回收广播接收器并避免ANR(Application Not Responding,应用无响应)
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关闭时,将显示链接文本。
- 过度唤醒