讓你從此不再懼怕ANR

原文鏈接 讓你從此不再懼怕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) 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是有實際操作意義的,遵循以下步驟來解決問題:

  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) 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 (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: 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_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.pswp.cn/news/214879.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/214879.shtml
英文地址,請注明出處:http://en.pswp.cn/news/214879.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

[排序篇] 冒泡排序

目錄 一、概念 二、冒泡排序 2.1 冒泡降序(從大到小排序) 2.2 冒泡升序(從小到大排序) 三、冒泡排序應用 總結 一、概念 冒泡排序核心思想&#xff1a;每次比較兩個相鄰的元素&#xff0c;如果它們不符合排序規則&#xff08;升序或降序&#xff09;則把它們交換過來。…

Linux內存管理(十七):percpu 機制(2)——動態分配

源碼基于:Linux5.4 約定: 芯片架構:ARM64內存架構:UMACONFIG_ARM64_VA_BITS:39CONFIG_ARM64_PAGE_SHIFT:12CONFIG_PGTABLE_LEVELS :3關聯博文: percpu機制(1)——框架實現 percpu機制(2)——動態分配 0. 前言 上一篇博文 我們剖析了 percpu 機制的整個框架,包括per…

大致人類應該是短時記憶和利用短時記憶控制利用周圍環境達到長期記憶的吧

這里寫目錄標題 圖代碼代碼解析圖 代碼 import timedef route_llm(route_text):passdef write_to_dask(one_sum, one_text, one_path

小程序嵌套H5

小程序嵌套H5 使用Hbuild x開發H5頁面項目里面使用了js-sdk工具包H5發布完成之后生成URL。新建一個小程序空項目&#xff0c;填寫小程序的appid。本地調試的時候如果報錯無法打開該網頁&#xff0c;那么需要勾選先的不校驗。發布體驗版本需要注意下面的兩個配置點。 使用Hbuild…

中通快遞單號查詢入口,將指定某天簽收的單號篩選出來

批量查詢中通快遞單號的物流信息&#xff0c;將指定某天簽收的單號篩選出來。 所需工具&#xff1a; 一個【快遞批量查詢高手】軟件 中通快遞單號若干 操作步驟&#xff1a; 步驟1&#xff1a;運行【快遞批量查詢高手】軟件&#xff0c;并登錄 步驟2&#xff1a;點擊主界面左…

Spring Boot中JdbcTemplate多數據源配置

作者簡介&#xff1a;大家好&#xff0c;我是擼代碼的羊駝&#xff0c;前阿里巴巴架構師&#xff0c;現某互聯網公司CTO 聯系v&#xff1a;sulny_ann&#xff08;17362204968&#xff09;&#xff0c;加我進群&#xff0c;大家一起學習&#xff0c;一起進步&#xff0c;一起對抗…

編譯 Flink代碼

構建環境 JDK1.8以上和Maven 3.3.x可以構建Flink&#xff0c;但是不能正確地遮蓋某些依賴項。Maven 3.2.5會正確創建庫。所以這里使用為了減少問題選擇 Maven3.2.5版本進行構建。要構建單元測試&#xff0c;請使用Java 8以上&#xff0c;以防止使用PowerMock運行器的單元測試失…

云計算核心技術

1.1 云計算的定義 云計算是目前業內的熱點概念&#xff0c;它以開放的標準和服務為基礎&#xff0c;以互聯網為中心&#xff0c;提供安全、快速、便捷的數據存儲和網絡計算服務&#xff0c;讓互聯網這片“云”上的各種計算機共同組成數個龐大的數據中心及計算中心。它可以被看成…

求職智能分析系統

本項目是一個基于Flask輕量級框架的計算機就業數據可視化分析平臺。 采用echarts和ajax等技術進行數據展示和用戶交互。

【電路筆記】-電位器

電位器 文章目錄 電位器1、概述2、電位器類型2.1 旋轉電位器2.2 滑塊電位器2.3 預設和微調電位器2.4 變阻器 3、電位器示例14、電位器作為分壓器5、電位器示例26、變阻器6、滑塊變阻器7、線性或對數電位器8、總結 當連接的軸物理旋轉時&#xff0c;電位計和變阻器的電阻值會發生…

一個簡單的Wireshark和TCP三次握手,為什么能難住阿里6年測試?

之前寫過一篇博客&#xff1a;用 Fiddler 來調試HTTP&#xff0c;HTTPS。 這篇文章介紹另一個好用的抓包工具wireshark&#xff0c; 用來獲取網絡數據封包&#xff0c;包括http,TCP,UDP&#xff0c;等網絡協議包。 記得大學的時候就學習過TCP的三次握手協議&#xff0c;那時候…

Vue中 v-show 和 v-if 有什么區別

Vue中的 v-show 和 v-if 一.v-show 與 v-if 原理分析v-show 原理v-if 原理 二、v-show 與 v-if 的共同點三、v-show 與 v-if 的區別四、v-show 與 v-if 的使用場景使用 v-show 的場景&#xff1a;使用 v-if 的場景&#xff1a; 五、v-show 與 v-if 的優缺點v-show優點&#xff…

kafka rebalance(再均衡)導致的消息積壓分析

起因&#xff1a; 某天&#xff0c;項目組收到大量的kafka消息積壓告警。查看了kafka日志后&#xff0c;發現 kafka不斷地 rebalance(再均衡)。 Rebalance (再均衡)&#xff1a; 分區的所有權從一個消費者轉移到另一個消費者&#xff0c;這樣的行為被稱為Rebalance (再均衡)…

修改汽車的控制系統實現自動駕駛,基于一個開源的汽車駕駛輔助系統實現全自動駕駛

修改汽車的控制系統實現自動駕駛,基于一個開源的汽車駕駛輔助系統實現全自動駕駛。 自動駕駛汽車依靠人工智能、視覺計算、雷達、監控裝置和全球定位系統協同合作,讓電腦可以在沒有任何人類主動的操作下,自動安全地操作機動車輛。 演示視頻: Openpilot :一個開源的汽車駕…

Socks5代理與代理IP的技術創新

隨著全球市場的開放和跨界電商的崛起&#xff0c;企業在出海過程中面臨著復雜多變的網絡環境和地域限制。在這一背景下&#xff0c;Socks5代理和代理IP等技術應運而生&#xff0c;成為助力企業突破網絡壁壘、實現出海目標的重要工具。本文將深入探討Socks5代理和代理IP在跨界電…

OpenSSL 3.x爆出漏洞,如何妥善應對?

10月25日&#xff0c;OpenSSL項目團隊發布了OpenSSL 3.x版中一個關鍵安全漏洞的修復程序。該修復程序已于11月1日正式發布。 由于OpenSSL有著極為廣泛的使用&#xff0c;該公告引起了很大反響。Akamai希望能通過本文幫助相關用戶了解情況&#xff0c;介紹有關檢測和緩解威脅的…

怎么消除視頻中所有的聲音?方法很簡單

當我們想把視頻中去掉聲音&#xff0c;可能有多種原因&#xff0c;也許需要制作一個無聲視頻&#xff0c;或者想在視頻中添加自己的音樂或解說&#xff0c;特別是一些搞笑解說&#xff0c;無論原因是什么&#xff0c;到底要怎么把視頻中所有的聲音都去除呢&#xff1f; 小編給…

計算機畢業設計 基于Web的網上購物系統(pc端仿淘寶系統)的設計與實現 Java實戰項目 附源碼+文檔+視頻講解

博主介紹&#xff1a;?從事軟件開發10年之余&#xff0c;專注于Java技術領域、Python人工智能及數據挖掘、小程序項目開發和Android項目開發等。CSDN、掘金、華為云、InfoQ、阿里云等平臺優質作者? &#x1f345;文末獲取源碼聯系&#x1f345; &#x1f447;&#x1f3fb; 精…

SVN優缺點詳解及版本控制系統選型建議

Subversion (SVN)是目前可用的眾多版本控制選項之一。本篇文章將全面概述什么是 SVN、SVN的歷史、SVN存儲庫是什么&#xff0c;以及在切換到SVN之前您應該謹慎考慮的潛在問題。 什么是Subversion&#xff08;SVN&#xff09;&#xff1f; Subversion軟件&#xff0c;也稱為SV…

管理類聯考——數學——真題篇——按知識分類——代數

文章目錄 2023真題(2023-09)-代數-一元二次方程-注意絕對值的有效性真題(2023-17)-代數-一元二次方程-舉反例真題(2023-18)-數列-等比數列真題(2023-24)-數列-等比數列2022真題(2022-03)-代數-整式-因式分解真題(2022-19)-數列-等比數列真題(2022-21)-數列-等比數…