宿州建设网站公司哪家好,公司网站主页设计,百度网页链接,各大搜索引擎收录入口原文链接 让你从此不再惧怕ANR 这篇文章是基于官方的Diagnose and fix ANRs翻译而来#xff0c;但也不是严格的翻译#xff0c;原文的内容都在#xff0c;又加上了自己的理解以及自己的经验#xff0c;以译注的形式对原文的作一些补充。 当一个Android应用的UI线程被阻塞时…原文链接 让你从此不再惧怕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是安卓系统的核心基础通信机制组件件间的通信IntentContentResolver应用与AMSPMS和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 receiversGPU挂起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实例来处理广播。所以应用冷启动时间是会被计算在超时时限内的从而慢的冷启动肯定会影响广播处理。通常开发者都会只关注应用启动后的情况比如渲染性能或者用户体验会忽略其他组件如BroadcastReceiverService以及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 { actandroid.inent.action.SCREEN_ON flg0x50200010 }受制于长广播超时(60~120秒)的标题例子:
Broadcast of Intent { actandroid.intent.action.TIME_SET flg0x25200010 }广播的超时时间是如何计算的
广播耗时时长测量从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。寻找以下信息
cmpreceiver classactbroadcast_intent
这里是一个广播超时ANR标签的例子
com.example.app.MyClass.myMethod
Broadcast of Intent { actandroid.accounts.LOGIN_ACCOUNTS_CHANGED
cmpcom.example.app/com.example.app.MyAccountReceiver }寻找运行onReceive方法的线程
如果使用Context.registerReceiver()时指定了自定义的handler那就会运行在此handler所依附的线程里。此外就是在主线程里。
实例异步接收器未被调度
这部分将逐步的演示如何调试一个广播接收超时ANR。
比如说ANR标签是像酱紫的
com.example.app.MyClass.myMethod
Broadcast of Intent {
actandroid.accounts.LOG_ACCOUNTS_CHANGED cmpcom.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 (tid26) Waitingat 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是有实际操作意义的遵循以下步骤来解决问题
找到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确定应用启动过程中服务组件或者其他地方是否有耗时或者阻塞操作通过检查主线程中的下面这些重要的方法调用
主线程栈帧中的方法调用背后的含义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或者视为假阳性。另外一个组件正在运行比如广播接收器。这种情况下主线程可能被这个组件阻塞着导致服务无法启动。
如果能看到关键的方法 调用并确定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 (tid13) BlockedWaiting 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 (tid1) 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 tid1 NativeMain threadIdle#00 pc 0x00000000000d8b38 /apex/com.android.runtime/lib64/bionic/libc.so (__epoll_pwait8)
#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: 300000libdebuggerd_client: failed to read status response from tombstoned: timeout reached?----- Waiting Channels: pid 7068 at 2022-10-18 02:21:37.US_SOCIAL_SECURITY_NUMBER0800 -----[...]簇集标签或者ANR报告里面没有栈帧的ANR是没有实际分析意义的。如果要调试可以去看其他的簇集信息因为如果一个问题足够明显的话那么它通常会有它自己的簇集标签存在。其他的可行方案就是查看Perfetto traces.
已知问题(Known issues)
在应用的进程里用计时器来测量广播的处理时间或者ANR的触发是行不通的因为系统是以异步的方式在监控着ANR。 **译注**这里的意思是不要想着取巧应用开发者的重点应该放在你的业务逻辑和性能优化上面借助平台提供的工具和方法来优化应用的代码逻辑。而像尝试在应用侧自己统计超时这种事情是行不通的因为系统以比较复杂的异步的方式在统计着超时应用侧不可能做到与系统侧一样的测量方法所以自己的统计就变得毫无意义要么不可行要么不准确。还是老老实实的优化好自己的代码吧。 更多的官方资料
Find the unresponsive threadKeep your app responsiveLayout resourceANRs
其他优质博文
钉钉 ANR 治理最佳实践 | 定位 ANR 不再雾里看花今日头条 ANR 优化实践系列 - 设计原理及影响因素Android ANR全解析华为AGC性能管理解决ANR案例集
原创不易打赏点赞在看收藏分享 总要有一个吧