稀有猿诉

十年磨一剑,历炼出锋芒,说话千百句,不如码二行。

让你从此不再惧怕ANR

这篇文章是基于官方的Diagnose and fix ANRs翻译而来,但也不是严格的翻译,原文的内容都在,又加上了自己的理解以及自己的经验,以译注的形式对原文的作一些补充。


当一个Android应用的UI线程被阻塞时间过长,系统就会发出一个臭名昭著的“应用程序未响应”(ANR, Application Not Responding”)错误。本文将讲述不同类型的ANR,如何分析以及如何解决。文中列出的所有的超时时间范围都是基于AOSP和Pixel设备;这些时间范围可能会依OEM厂商而不同。

需要注意的是,当分析ANR的根因时,区分系统原因应用本身的原因是很有帮助的。 当整个系统处于一个糟糕状态时,下面这些问题可能会引发ANR:

  • 系统服务内部的一些瞬时问题(Transient issues)就会导致通常很快的binder call变得非常慢。
  • 系统服务的问题以及较高的系统负载会导致应用程序的线程无法被正常的调度。

译注:瞬时问题Transient issue是指一些服务运行时出现了一些瞬时的小错误比如服务器的网络抽风(闪断又闪连),或者一个系统服务的I/O错误,但可能会导致客户无法正常的获得响应。这里要这样来理解,服务(servers)一般都是长时间运行的,它是有可能会发生一些小错误的,瞬时的很快就恢复了,但如果客户恰好在此时来请求就不会得到响应。尽管这对于服务来说是一个可以忽略的小错误,毕竟它是长时间运行的,几秒钟的小错误不影响它本身的运行,但对客户侧的影响却是较大,对客户侧来说就是请求得不到响应。

如果可以的话,区分系统问题还是应用问题的好方法就是使用Perfetto traces:

  • 通过查看在Perfetto跟踪的是运行中还是未运行的线程的状态来判断应用的主线程有没有被正常的调度。
  • 查看系统进程system_server的线程,看有没有锁竞争之类的问题。
  • 对于耗时的(跨进程调用)binder calls,查看一下是否存在应答进程,以及为何它会耗时。

译注:很多重要的系统服务都在system_server进程里面,如负责创建调度所有组件的AMS(Activity Manager Service),包管理PMS(Package Manager Service),窗口管理WMS(Window Manager Service)等等,system_server进程本来的load其实不轻。再加上很多OEM定制化的功能也必须要在AMS处做事情(如hook或者拦截),导致system_server并不比应用程序少引发问题,而一旦system_server有耗时操作或者在等待锁,会导致整个系统处于极度卡顿状态,这时事件的派发,组件的创建,生命周期的调度,以及WMS的焦点处理等等正常的逻辑都不可能得到及时的流转和响应。这种时候任何一个应用都可能随时发生ANR,但应用本身却都是idle状态,问题是在system_server这一侧。

Binder是安卓系统的核心基础通信机制,组件件间的通信,Intent,ContentResolver,应用与AMS,PMS和WMS等等之间的交互都是通过binder call来进行的,常规情况下大部分时候binder call都没有问题会很快问题,但如果binder另一头的某个服务发生了问题,即使是瞬时问题,也会导致binder call被阻塞或者变慢,这时就可能引发应用侧的ANR。

需要厘清概念,系统服务(services)与进程并不是同一回事,也不是一一对应的关系。系统服务是安卓系统架构上的模块,都分布于框架层,支撑着系统的运转。而进程则是CPU(准确的说是操作系统内核)运行和调度的基本单元(进程则再细分为线程)。一个系统服务可能独立占用一个进程,比如像Media Service(mediaserver)CameraService(cameraserver),也可能会生成几个进程;当然 也有可能几个服务都在同一个进程里面,比如前面提到的与应用程序最为密切相关的三大服务AMS, WMS和PMS。当一个服务必须要有独立进程的时候,就会为它创建独立的进程,比如像CameraService,在Android O以前是没有独立进程的,它活在mediaserver里,后来才有独立的进程cameraserver。

服务是架构上的逻辑概念,而进程和线程是从硬件(CPU)角度看到的代码的执行。ANR是由于进程(准确的说是线程,进程由至少一个线程组成)卡顿或者被阻塞导致的。调试的手段也都是从代码执行的角度,把线程的栈帧转储出来(stack trace dump),以查看是被哪 个函数阻塞了。

输入派发超时(Input dispatch timeout)

输入派发无响应发生在应用的主线程无法及时地响应一个输入事件,如滑动手势或者物理按键。因为当输入派发超时发生时应用是在前台的,所以这类超时总是对用户可见的,所以想办法规避是很重要的。

默认超时时间:5秒

输入派发超时无响应通常是由于主线程的问题引起的。如果主线程因为等待获取某个锁而阻塞,锁的持有线程也包含在内。遵循以下最佳实践以防止输入派发未响应:

  • 主线程不要进行可能会阻塞或者耗时的操作。可以考虑使用严格模式StrictMode来捕捉主线程的一些异常的行为。
  • 尽可能的减少主线程和其他线程之间的锁竞争。
  • 在主线程尽可能减少非UI相关的操作,比如当处理广播(Broadcasts)时或者处理服务时(Services)。

常见的根因

这里列出一些输入派发无响应常见的根因以及修复建议。

根因 表象 修复建议
耗时跨进程调用slow binder call 主线程执行了一个耗时同步binder call 把这个调用放到非主线程,或者优化一下这个调用,如果你负责这个API的话
很多连续的binder calls 主线程执行了很多连续的跨进程调用 不要在一个密集的循环中执行binder call
阻塞式的I/O 主线程执行了阻塞式的I/O,如数据库操作或者网络请求 把所有阻塞式I/O调用放到非主线程里
锁竞争 主线程因为等待获取某个锁而阻塞 减少主线程与其他线程之间的锁竞争,优化其他线程中的耗时代码
耗时的帧 在一帧里面做太多的渲染,导致严重的丢帧 减少帧渲染的工作。不要用超过O(n2)的算法。用一些高效的组件来进行滑动和分页,比如Jetpack中的Paging library
被其他组件阻塞 其他的组件比如广播接收器(BroadcastReceiver)正在运行并阻塞着主线程 主线程尽量不要做非UI操作,另起一个线程运行broadcast receivers
GPU挂起 GPU挂起是一个系统问题或者硬件问题,会导致渲染被阻塞,因此也会引发输入派发ANR 很不幸的是在应用程序侧是无法搞定这个问题的。唯一的可能就是联系对应厂商。


如何调试

通过查看在Google Play Console和Firebase Crashlytics中的ANR簇标来开始调试。簇集会包含疑似引发ANR的最多的栈帧。

注意:忽略簇集是”navivePollOnce”和”main thread idle”的输入派发ANR。这类标志通常是关联着栈帧转储太晚的ANRs,没有可操作的提示所以要忽略掉。一般来说,真正的ANR会在其他簇集里,所以问题并不会被掩盖。详细信息可参见nativePollOnce部分

译注:这篇文档是谷歌官方的,所以它自然会使用谷歌官方的应用后台(Google Play Console)和统计分析(Firebase Crashlytics)工具,对于大部分国内的开发者来说这两个东西可能比较陌生。但没关系,原理是相通的,国内也有很多应用异常统计工具和后台,或者一些本地工具抓取的日志,形式是不限的,只要能收集到类似的栈帧(stack traces)就可以用于分析调试ANR。栈帧(stack frame或者stack trace)就是线程里面的函数调用栈,比如a()->b()->c()->d()这样的函数调用,所有的异常统计工具或者日志工具都能抓取出来某一时刻每个线程的栈帧,这也称之为栈帧转储(stack frame dump)。

下面的流程图展示如何确定一个输入派发超时ANR的根因:

图1. 如何调试一个输入派发无响应ANR

Play vitals能够探测并帮助调试这些常见ANRs原因中的一部分。比如说,如果vitals探测到一个ANR是因为锁竞争,它会总结这些问题并在ANR Insights部分给出建议的修复方法。

图2. Google Play vitals ANR探测

译注:输入派发超时ANR发生的时候应用一定是在前台的,并且用户正在交互。因此重点要看主线程里面的可能的耗时操作,对于系统侧的问题以及关键的生命周期方法则一般不太相干,因为这时生命周期一般都走完了,处理常规的交互阶段。

找不到有焦点的窗口(No focused window)

像触摸等的事件通过命中测试后会直接发送到相关窗口,而像硬件按键事件则需要一个目标(窗口)。这个目标就是指有焦点的窗口。每一个显示器每一时刻只有一个有焦点的窗口,并且常常就是用户当前正在使用的那个。如果找不到有焦点的窗口,输入服务会触发一个”No focused window ANR”。找不到焦点窗口ANR是输入派发无响应中的一种。

默认超时时间:5秒。

常见的原因

无焦点窗口ANRs通常由以下原因导致:

  • 应用启动做了太多耗时操作,还没有渲染出来第一帧。
  • 应用的主窗口无法获取焦点。如果一个窗口被使用了标志位FLAG_NOT_FOCUSABLE,那么用户 就无法发送按键事件或者触摸事件到这个窗口上面。
1
2
3
4
5
override fun onCreate(savedInstanceState: Bundle) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    window.addFlags(WindowManager.LayoutParams.FLAG_FLAG_NOT_FOCUSABLE)
}

译注:No focused window说明应用应在前台而未在前台,或者不应该在前台而在前台,这类ANR最容易发生在生命周期方法执行太慢导致input与window焦点状态不同步导致的。所以重点要看应用的关键生命周期回调方法是否有耗时操作,比如onCreate()/onDestroy(),onStart()/onStop(),以及特别的onResume()/onPause()。可以与上面的输入派发超时进行对比,可以发现这两类ANR分析的侧重点并不一样。

广播接收器超时(Broadcast receiver timeout)

广播接收器ANR发生在当一个广播接收器无法及时的响应一个广播。对于一个同步的接收器,或者没有调用goAsync)的receivers,超时的意思是onReceive())方法未能及时的执行完。对于异步接收器,或者调用了goAsync的receivers,超时的意思是PendingResult.finish未能及时的被调用。

广播接收器ANRs经常发生在这些线程中:

  • 主线程,问题会是应用启动太慢
  • 运行broadcast receiver的线程,问题会是onReceive执行太慢
  • 广播的后台线程,问题会是执行goAsync的代码太耗时了

遵循这些最佳实践来避免广播接收器ANRs:

  • 保证快速应用启动,因为应用启动时间也会被计算在ANR的超时时间里,如果应用是被唤醒来处理广播。
  • 如果使用了goAsync,要确保PengingResult.finish早点被调用。这跟同步receivers一样都受超时时间影响。
  • 如果使用了goAsync,要确保工作线程没有开启耗时操作或者阻塞性的操作。
  • 考虑在非主线程里面调用registerReceiver)以免阻塞主线程中的代码执行。(这里的意思是要为广播提供一个非主线程的Handler,这是广播处理回调onReceiver运行的线程。如不提供Handler将会在主线程中运行 —译注)

译注:广播接收器是一个独立的组件,用于任何时候接收广播事件并进行处理,包括应用还未运行时。因此,如果应用还未有运行,那么要响应广播,必须先把应用唤起(创建进程,并创建Application实例),然后才能创建receiver实例来处理广播。所以应用冷启动时间是会被计算在超时时限内的,从而慢的冷启动肯定会影响广播处理。通常开发者都会只关注应用启动后的情况,比如渲染性能或者用户体验,会忽略其他组件如BroadcastReceiver,Service以及ContentProvider是与Activity一样的平台级别的组件,它们都能单独的运行,但它们毕竟都是在同一个应用里面,要运行在同一进程和同一个Application实例下面,所以在运行这些组件前AMS是需要先唤起应用,应用的启动会影响着所有的四大组件。另外要注意,尽管可以用”android:process”给组件(通常是给Service和ContentProvider)指定单独的进程,但冷启动的影响也是存在的,同样需要创建进程和Application实例,并且其实主进程也是被会唤起的。

超时时限(Broadcast receiver timeout)

广播接收超时时限取决于前台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。如果这他二进制位是1就说明前台标志被启用了。

受制于短时广播超时时间(10~20秒)的标题例子:

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

受制于长广播超时(60~120秒)的标题例子:

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

广播的超时时间是如何计算的

广播耗时时长测量从system_server把广播派发给应用时开始,到当应用完成广播的处理时结束。如果应用程序的进程没在运行,还需要把应用冷启动时间计算在ANR的超时时间里面。因此,缓慢的应用启动也可能会导致广播接收超时ANR。

下面这张图展示了广播接收器的时间线与应用进程的对齐关系:

图3. 广播接收器时间线

ANR超时时间测量当接收器处理完广播时就结束,具体这个什么时候算结束取决于是同步接收器还是异步接收器:

  • 对于同步接收器,当onReceive方法返回时测量就结束了。
  • 对于异步接收器,当PendingResult.finish被调用时就结束。

图4. 同步接收器和异步接收器的ANR超时测量结束时间点

常见的根因

这里列出广播接收超时ANR的一些常见根因以及修复建议。

根因 适用于 表象 建议的修复方式
缓慢的应用启动 所有接收器 应用在冷启动耗时太多 优化应用的冷启动
onReceive未被调度 所有接收器 广播接收器线程正忙于其他操作无法执行onReceive 不要在接收器的线程里面做长时间的耗时操作(放到其他工作线程里去)
缓慢的onReceive 所有的接收器,主要是同步接收器 开始执行onReceive了,但因为被阻塞了或者执行的太慢,无法及时的完成并返回 优化缓慢的onReceive代码
异步接收器未被调度 goAsync()接收器 onReceive要在一个被阻塞的工作线程池中执行,所以始终得不到执行 优化阻塞的代码或者binder call,或者用不同的线程来当作广播的工作线程
工作线程太慢或者被阻塞 goAsync()接收器 当处理广播时,在工作线程池中有耗时操作或者阻塞代码。因此,PendingResult.finish()无法及时被调用 优化缓慢的异步接收器代码
忘记调用PendingResult.finish() goAsync()接收器 代码的逻辑中没有调用finish() 保证finish()被调用到


如何调试

基于簇集标签(cluster signature)和ANR报告,可以定位到广播接收器运行的线程,然后再定位到未执行的代码或者运行缓慢的代码。

注意:不要忽略”nativePollOnce”或者”main thread idle”的簇集标签。Google Play Console和Firebase Crashlytics的ANR标签里面的栈帧通常都是从主线程中获取生成的。但是,广播接收器可能运行在非主线程或者调用了goAsync()(也即转成了异步接收器—译注)。因此,这些簇集标签仍然有实际价值,可以查看一下栈帧里面的相关线程。

下面的流程图展示了如何确定一个广播接收超时ANR的根因:

图5. 如何调试一个广播超时ANR

找到接收器的代码

Google Play Console会在ANR簇集标签里面显示接收器的类名和广播Intent。寻找以下信息:

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

这里是一个广播超时ANR标签的例子:

1
2
3
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()时指定了自定义的handler,那就会运行在此handler所依附的线程里。此外,就是在主线程里。

实例:异步接收器未被调度

这部分将逐步的演示如何调试一个广播接收超时ANR。

比如说ANR标签是像酱紫的:

1
2
3
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]“在主要负责处理这个广播。查看栈帧,可以发现所有四个后台线程(background threads)的模式是一样的:它们都执行了一个阻塞式的调用getDataSync。因为所有的后台线程都被占用着,这个广播无法被及时处理,最后发生了ANR。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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会如此之慢,然后优化
  • 不要在四后台线程中都执行getDataSync
  • 更为通用的做法是,保证后台线程池中不要执行长时间的耗时操作
  • 为goAsync任务设计一个专用线程池
  • 使用一个无数量限制的线程池,而不是限量为4的后台线程池

实例:应用启动缓慢

应用启动缓慢可能会导致几个不同类型的ANR,以广播接收超时ANR和执行服务超时ANR最为显著。如果你在主线程的帧中看到了ActivityThread.handleBindApplication,那么这个ANR的根因很有可能就是启动慢造成的。

译注:四大组件(Activity, Service, BroadcastReceiver和ContentProvidier)都是平台能直接识别的组件,均可由AMS直接启动运行,但它们都是应用的一部分,如果应用尚未运行,那么AMS必须先要创建进程,并创建Application实例,这都需要花费时间,会耗费更久,甚至引发ANR,如果冷启动过程中有耗时操作。所以优化应用启动是性能优化的基石。

执行服务超时(Exceute service timeout)

当应用程序的主线程无法及时的启动一个Service时就会发生执行服务超时ANR。具体来说,就是一个服务无法在一定时限范围内完成onCreate()或者onStartCommand()或者onBind()的执行。

默认超时时间:前台服务(Foreground Service)是20秒; 后台服务(Background Service)是200秒。ANR超时时间包括应用冷启动,以及onCreate(),onBind()和onStartCommand的调用。

遵循如下最佳实战来规避执行服务ANR:

  • 确保应用启动很快,因为如果一个应用被唤起来运行服务组件,启动时间也会被计算在超时时间内。
  • 确保服务的onCreate(),onBind()和onStartCommand()执行的都很快。
  • 不要在主线程里执行来自其他组件的耗时操作或者阻塞式操作,这些操作会阻碍服务的快速启动。

常见的根因

下表列出执行服务超时ANR的常见根因和修复建议:。

根因 表象 建议的修复
缓慢的应用启动 应用冷启动时间过长 优化应用启动速度
缓慢的onCreate(),onStartCommand和onBind 服务组件的onCreate(),onStartCommand()和onBind()在主线程执行了耗时操作 优化代码,或者把耗时操作从这些关键的方法中移出去
未被调度(在执行onStart()之前主线程就被阻塞了) 在服务启动之前,主线程就被其他组件级阻塞了 把其他组件的工作移出主线程。优化其他组件的阻塞代码


如何调试

从Google Play Console和Firebase Crashlytics中的簇集标签和ANR报告,基于主线程当时的运行状态,通常就能确定ANR的根因。

注意:忽略标签是”nativePollOnce”和”main thread idle”的执行服务ANR簇集。这些簇集通常是栈帧捕获的太晚,无实际参考意义。真实的ANR栈帧可能会在其他的簇集里,所以问题并不会被掩藏。详细参见nativePollOnce部分。

下面的流程图描述了如何调试一个执行服务超时ANR。

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

如果发现某个执行报务ANR是有实际操作意义的,遵循以下步骤来解决问题:

  1. 找到ANR簇集标签中的服务组件。在Google Play Console里,服务组件类型会显示在ANR标签里。在后面的这个例子里,类型就是com.example.app/MyService。
1
2
com.google.common.util.concurrent.Uninterruptibles.awaitUninterruptibly
Executing service com.example.app/com.example.app.MyService
  1. 确定应用启动过程中,服务组件或者其他地方是否有耗时或者阻塞操作,通过检查主线程中的下面这些重要的方法调用
主线程栈帧中的方法调用 背后的含义
android.app.ActivityThread.handleBindApplication 应用正在启动,ANR由启动太慢引起
.onCreate()
[….]
android.app.ActivityThread.handleCreateService
服务正在被创建中,所以ANR是由缓慢的onCreate()引起的
.onBind()
[….]
android.app.ActivityThread.handleBindService
服务正在被绑定中,所以ANR是由缓慢的onBind()引起的
.onStartCommand()
[….]
android.app.ActivityThread.handleServiceArgs
服务正在被启动中,所以ANR是由缓慢的onStartCommand()引起的


举个粟子,如果在类MyService里的onStartCommand执行缓慢,主线程栈帧会像酱婶儿的:

1
2
3
4
5
6
7
8
9
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或者视为假阳性。
  • 另外一个组件正在运行,比如广播接收器。这种情况下主线程可能被这个组件阻塞着,导致服务无法启动。

  • 如果能看到关键的方法 调用并确定ANR发生的地点,检查主线程的栈帧以找到缓慢的操作并把它们从关键的方法中移出去。

关于服务的更多信息,可以看下面这些链接:

内容提供程序无响应(Content Provider not responding)

当一个远端内内容提供程序响应查询(query)时花费超过时限,内容提供程序ANR就会发生,且会被杀掉。

默认超时时间:内容提供程序通过ContentProviderClient.setDetectNotResponding指定的。ANR超时时限包括远端内容提供程序执行查询的时间,以及如果远端应用还未启还包括它的冷启动时间,加在一起的总时间。

遵循下面这些最佳实践来规避内容提供程序ANR:

  • 确保应用启动很快,因为如果应用未运行时会被唤起,冷启动时间也会被计算在超时时间内。
  • 确保内容提供程序的查询能很快执行完。
  • 不要执行大量的并发阻塞式的binder call,因为这会阻塞应用的所有的binder线程。

译注:内容提供程序Content provider都是要经过跨进程调用(binder call),尽管可能并没有真正的在另外一个进程里。因为我们使用ContentProvider的时候都是通过另一个API ContentResolver来完成,而ContentResolver是通过binder call来与ContentProvider通信的,无论是否真的跨进程。所以,ContentProvider就像一个服务器一样是远端的一侧提供内容,而应用程序(使用者)是客户端一侧需要内容。内容提供程序可能同时服务着不同的客户请求,比如像系统通用的内容提供程序ContactsProvider或者MediaProvider可能同时会有大量的应用请求查询,每一个请求都需要执行binder call,因此内容提供程序可能会同时执行着大量的binder call(它需要查询结果,并把结果以binder call的形式返回给请求方)。所以对于内容提供程序来说,查看binder call的运行状态对于解决ANR问题以及排查性能问题都是非常有帮助的。

常见根因

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

根因 表象 信号 建议的修复方式
缓慢的查询 内容提供程序执行耗时太长或者被阻塞 binder线程里有android.content.ContentProvider\$Transport.query栈帧 优化查询或者查出什么东西在阻塞着binder线程
应用启动太慢 内容提供程序启动耗时太久 主线程里有ActivityThread.handleBindApplication栈帧 优化应用启动
Binder线程耗尽了,所有的binder线程都被占用着 所有的binder线程都被占用着服务着其他的同步请求,因此内容提供程序binder调用无法执行 应用未启动起来,所有的binder线程都被占用,内容提供程序也未能启动起来 减小binder线程的负载。也就是说执行更少一些的外发同步binder调用或者在处理到来的调用时少做一些操作。


如何调试

要想调试一个内容提供程序ANR,使用Google Play Console或者Firebase Crashlytics中的簇集标签和ANR报告,并用来查看主线程以及binder线程都在做什么。

下面的流程图描述如何调试一个内容提供程序ANR:

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

下面的代码块展示了当被一个缓慢的内容提供程序查询阻塞时,binder线程的状态。在这个例子里,内容提供程序的查询正在等待一个打开数据库的锁。

1
2
3
4
5
6
7
8
9
10
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)

下面的代码块展示了当被缓慢的应用启动阻塞时,binder线程的状态。在这个例子里,应用启动因为dagger初始化时的锁竞争而变得很慢。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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)

缓慢的作业响应(Slow job response)

当应用响应JobService.onStartJob()或者JobService.onStopJob耗时太久,或者用JobService.setNotification()提供通知时耗时太久,都会引发缓慢的作业响应ANR发生。这说明应用的主线程因为其他操作而被阻塞了。

如果问题是与JobService.onStartJob()或者JobService.onStopJob()有关系,就要检查下主线程的情况。如果问题与JobService.setNotification()有关系,要保证它尽可能的快速的被调用到。在提供通知之前 不要做很多其他事情。

译注:JobService是Android 5.0 API 21时增加的一个专门用于后台作业的一个Service的子类。上面提到的是都是它的一些回调,与一些其他的回调类似,这些回调必须快速执行完毕,因为JobSchedule内部需要做一些资源回收之类的工作,所以这些回调不允许被阻塞。

隐秘的ANRs

有时候搞不清楚为啥ANR会发生,或者在簇集标签和ANR报告中找不到足够的信息去调试。遇到这些情况,还是可以采取一些步骤以确定这些ANR是否是值得处理的。

消息队列是空闲(Message queue idle)的或者正处理轮询中(nativePollOnce)

如果你在栈帧信息中发现android.os.MessageQueue.nativePollOnce,这通常说明疑似无响应的线程实际上是空闲的或者在等待队列中的消息。在Google Play Console里面,ANR的细节是酱紫的:

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

举个粟子,如果主线程是空闲的,栈帧是酱紫的:

1
2
3
4
5
6
7
8
9
10
11
12
"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版本的Pixels设备上这个延迟大约在100ms,但也可能超过1秒。Android 14版本的Pixels设备上这个延迟小于10ms。
  • 线程归因错误:用于构建ANR标签的线程并不是实际上触发ANR的无响应线程。这种情况下,尝试确定一下这个ANR是否是如下的类型:
    • 广播接收超时
    • 内容提供程序无响应
    • 找不到带焦点的窗口
    • 系统侧问题:由于系统负载太重或者系统服务有问题而导致应用进程无法被调度。

没有栈帧(No stack frames)

有一些ANR报告里面没有包含与ANR相关的栈帧,这说明在生成ANR报告时栈帧转储失败了。有很多可能的原因会导致栈帧丢失:

  • 转储栈帧太耗时了,所以超时了
  • 在栈帧转储完成之前进程就挂了或者被杀掉了
1
2
3
4
5
6
7
8
9
10
11
12
[...]

--- 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 traces.

已知问题(Known issues)

在应用的进程里用计时器来测量广播的处理时间或者ANR的触发是行不通的,因为系统是以异步的方式在监控着ANR。

译注:这里的意思是不要想着取巧,应用开发者的重点应该放在你的业务逻辑和性能优化上面,借助平台提供的工具和方法来优化应用的代码逻辑。而像尝试在应用侧自己统计超时这种事情是行不通的,因为系统以比较复杂的异步的方式在统计着超时,应用侧不可能做到与系统侧一样的测量方法,所以自己的统计就变得毫无意义(要么不可行,要么不准确)。还是老老实实的优化好自己的代码吧。

更多的官方资料

其他优质博文

Comments