让你从此不再惧怕ANR

news2024/7/6 9:43:12

原文链接 让你从此不再惧怕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(n^2)的算法。用一些高效的组件来进行滑动和分页,比如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,那么用户 就无法发送按键事件或者触摸事件到这个窗口上面。
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秒)的标题例子:

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

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

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标签的例子:

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标签是像酱紫的:

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。

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。
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执行缓慢,主线程栈帧会像酱婶儿的:

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或者视为假阳性。
  • 另外一个组件正在运行,比如广播接收器。这种情况下主线程可能被这个组件阻塞着,导致服务无法启动。
  1. 如果能看到关键的方法 调用并确定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线程的状态。在这个例子里,内容提供程序的查询正在等待一个打开数据库的锁。

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初始化时的锁竞争而变得很慢。

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的细节是酱紫的:

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

没有栈帧(No stack frames)

有一些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 traces.

已知问题(Known issues)

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

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

更多的官方资料

  • Find the unresponsive thread
  • Keep your app responsive
  • Layout resource
  • ANRs

其他优质博文

  • 钉钉 ANR 治理最佳实践 | 定位 ANR 不再雾里看花
  • 今日头条 ANR 优化实践系列 - 设计原理及影响因素
  • Android ANR全解析&华为AGC性能管理解决ANR案例集

原创不易,打赏点赞在看收藏分享 总要有一个吧

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1302462.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Jeecg Boot 3 - 第二天】1.1、后端 docker-compose 部署 JEECGBOOT3

一、场景 二、实战 ▶ 2.1 修改配置文件 &#xff1e; 目的一&#xff1a;将 dev 变更为生产环境 prod &#xff1e; 目的二&#xff1a;方便spring项目调用docker同个network下的redis和mysql ▶ 2.2 编写dockerfile ▶ 2.3 编写docker-compose.yaml ▶ 2.4 打…

老师们居然这样把考试成绩发给家长

教育是一个复杂而多元的过程&#xff0c;其中考试成绩的发布和沟通是教育过程中的一个重要环节。然而&#xff0c;有些老师在发布考试成绩时&#xff0c;采取了一些不恰当的方式&#xff0c;给家长和学生带来了不必要的困扰和压力。本文将探讨老师们不应该采取的发布考试成绩的…

Docker | 自定义网络

✅作者简介:大家好,我是Leo,热爱Java后端开发者,一个想要与大家共同进步的男人😉😉 🍎个人主页:Leo的博客 💞当前专栏:Docker系列 ✨特色专栏: MySQL学习 🥭本文内容: Docker | 自定义网络 📚个人知识库: 知识库,欢迎大家访问 1.前言 大家好,我是Leo哥…

最小体力消耗路径(广度优先搜索)

最小体力消耗路径 看见这题第一眼-动态规划&#xff0c;再看BFS。 用动态规划做的话不能一次保证当前位置能获得最小的最大值&#xff0c;因为需要周围的四个&#xff08;或者两个&#xff09;元素值。 这里我纯用的BFS&#xff0c;宽度优先搜索。类似于n皇后问题。见代码吧…

Course3-Week1-无监督学习

Course3-Week1-无监督学习 文章目录 Course3-Week1-无监督学习1. 欢迎1.1 Course3简介1.2 数学符号约定 2. K-means算法2.1 K-means算法的步骤2.2 代价函数2.3 选择聚类数量2.4 代码实例-图像压缩 3. 异常检测3.1 异常检测的直观理解3.2 高斯分布3.3 异常检测算法3.4 选取判断阈…

紧固件的标准有哪些

紧固件的行业标准 紧固件是一个涉及几乎所有结构、机械和人们日常使用的产品的稳定性、安全性和结构完整性的广泛话题。紧固件作为一种功能强大、高效的机械设备&#xff0c;其功能和特性的规格多种多样&#xff0c;根本无法低估。由于紧固件是工程和机械的支柱&#xff0c;因此…

如何使用Imagewheel本地搭建一个简单的的私人图床公网可访问?

文章目录 1.前言2. Imagewheel网站搭建2.1. Imagewheel下载和安装2.2. Imagewheel网页测试2.3.cpolar的安装和注册 3.本地网页发布3.1.Cpolar临时数据隧道3.2.Cpolar稳定隧道&#xff08;云端设置&#xff09;3.3.Cpolar稳定隧道&#xff08;本地设置&#xff09; 4.公网访问测…

新版Spring Security6.2架构 (三) - Authorization

前言 书接上文&#xff0c;在经过了authentication后就是authorization了&#xff0c;本文还是对官网文档authorization的一个架构翻译和个人理解&#xff0c;后续的博客在写具体使用例子&#xff0c;从数据中认证&#xff0c;融合authentication和authorization的概念。 Aut…

堆的相关时间复杂度计算(C语言)

目录 前言 建堆的时间复杂度 向上调整建堆的时间复杂度 向下调整建堆的时间复杂度 维护堆的时间复杂度 top K问题的时间复杂度 前言 在前面的三篇文章中我们成功的实现了堆排序的升降序以及基于堆的top K问题&#xff0c;现在我们来解决它们的时间复杂度问题 建堆的时间…

【精选】 VulnHub (超详细解题过程)

&#x1f36c; 博主介绍&#x1f468;‍&#x1f393; 博主介绍&#xff1a;大家好&#xff0c;我是 hacker-routing &#xff0c;很高兴认识大家~ ✨主攻领域&#xff1a;【渗透领域】【应急响应】 【python】 【VulnHub靶场复现】【面试分析】 &#x1f389;点赞➕评论➕收藏…

前后端请求之nginx配置

问题&#xff1a; 前端发送的请求&#xff0c;是如何请求到后端服务器的&#xff1f; 如&#xff0c;前端请求地址&#xff1a;http://loclhost/api/employee/login&#xff1a; 后端相应请求地址&#xff1a;http://loclhost:8080/admin/employee/login 回答&#xff1a; …

Firmware Analysis Plus (Fap)固件模拟安装教程(最新)

最近在搞IoT的研究&#xff0c;但是难在设备比较难弄&#xff0c;只有固件&#xff0c;而没有设备&#xff0c;买吧&#xff0c;又太费钱&#xff0c;不划算。好在有很多项目可以在模拟环境中运行固件。但是几乎没有一个平台能够模拟所有硬件设备。IoT产品的架构也不尽相同。 …

Idea spring项目中 resource图标错误解决方案

1.resources错误显示示例 2.resources正确显示示例 3.解决方案 第一步&#xff1a; 第二步&#xff1a; 点击完成即可。

Zidebactam sodium salt β-内酰胺酶抑制剂 1706777-46-9科研

Zidebactam sodium salt β-内酰胺酶抑制剂 1706777-46-9 &#xff08;源自星戈瑞&#xff09; ATH686 FLT3 抑制剂 853299-52-2 Pelitrexol 抑制剂 446022-33-9 TBT1 转运蛋白抑制剂 52535-76-9 HFY-4A HDAC 抑制剂 2094810-82-7 SDR-04 BET 抑制剂 879593-54-1 Phthala…

Java项目-瑞吉外卖Day3

填充公共字段&#xff1a; 目的&#xff1a;由于某些属性&#xff0c;例如createdTime这些需要填充的字段会在多个地方出现&#xff0c;所以考虑使用公共字段自动填充的办法减少重复代码。 在对应属性上加入TableField注解。通过fill字段表明策略&#xff0c;是插入/更新的时候…

国内大厂机器人赛道产品

大疆 大疆无人机自然不必说&#xff0c;除此之外大疆搞机甲大师&#xff0c;教育机器人。 字节 当前字节在机器人领域只是初步探索阶段&#xff0c;目前尚未发布相关产品&#xff08;截止至23.12&#xff09;。 管理层想法&#xff1a; 跟已有业务做结合&#xff0c;服务好…

机器学习之全面了解回归学习器

我们将和大家一起探讨机器学习与数据科学的主题。 本文主要讨论大家针对回归学习器提出的问题。我将概要介绍&#xff0c;然后探讨以下五个问题&#xff1a; 1. 能否将回归学习器用于时序数据&#xff1f; 2. 该如何缩短训练时间&#xff1f; 3. 该如何解释不同模型的结果和…

Swift “黑魔法”之动态获取类实例隐藏属性的值

概览 在 Swift 代码的调试中,我们时常惊叹调试器的无所不能:对于大部分“黑盒”类实例的内容,调试器也都能探查的一清二楚。 想要自己在运行时也能轻松找到 Thread 实例“私有”属性的值吗(比如 seqNum)? 在本篇博文中您将学到如下内容: 概览1. 借我,借我,一双慧眼吧…

重磅!2023中国高校计算机大赛-人工智能创意赛结果出炉

目录 中国计算机大赛-人工智能创意赛现场C4-AI大赛颁奖及留影800个AI应用&#xff1f;这届大学生真能“搞事情”AI原生时代&#xff0c;百度要再培养500万大模型人才 中国计算机大赛-人工智能创意赛现场 12月8日&#xff0c;杭州&#xff0c;一位“白发老人”突然摔倒在地&…

UIUC北大最新工作Radarize:室内环境大规模雷达SLAM

0. 笔者个人体会 相对视觉和LiDAR SLAM来说,基于Radar的SLAM较为小众。但视觉SLAM对光照和低纹理区域敏感,在室内使用还有隐私问题。LIDAR SLAM在长走廊、雾烟尘等退化环境效果不好。所以在一些专用领域,Radar SLAM还是有不可替代的价值。 今天笔者将为大家分享伊利诺伊大学…