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