摘要:此前滴滴出行安卓端曾長期受此的影響,每天有一些用戶會因此遇到,經(jīng)過深度分析,最終找到有效解決方案。方法內(nèi)盡量減少耗時以及線程同步時間。減少高優(yōu)先級線程的創(chuàng)建和使用,降低高優(yōu)先級線程的使用率。
出品 | 滴滴技術(shù)
作者 | 江義旺
前言:隨著安卓 APP 規(guī)模越來越大,代碼越來越多,各種疑難雜癥問題也隨之出現(xiàn)。比較常見的一個問題就是 GC finalize() 方法出現(xiàn) java.util.concurrent.TimeoutException,這類問題難查難解,困擾了很多開發(fā)者。那么這類問題是怎么出現(xiàn)的呢?有什么解決辦法呢?這篇文章為將探索 finalize() timeout 的原因和解決方案,分享我們的踩坑經(jīng)驗,希望對遇到此類問題的開發(fā)者有所幫助。
在一些大型安卓 APP 中,經(jīng)常會遇到一個奇怪的 BUG:ava.util.concurrent.TimeoutException
其表現(xiàn)為對象的 finalize() 方法超時,如 android.content.res.AssetManager.finalize() timed out after 10 seconds 。
此前滴滴出行安卓端曾長期受此 BUG 的影響,每天有一些用戶會因此遇到 Crash,經(jīng)過深度分析,最終找到有效解決方案。這篇文章將對這個 BUG 的來龍去脈以及我們的解決方案進(jìn)行分析。
finalize() TimeoutException 發(fā)生在很多類中,典型的 Crash 堆棧如:
這類 Crash 都是發(fā)生在 java.lang.Daemons$FinalizerDaemon.doFinalize 方法中,直接原因是對象的 finalize() 方法執(zhí)行超時。系統(tǒng)版本從 Android 4.x 版本到 8.1 版本都有分布,低版本分布較多,出錯的類有系統(tǒng)的類,也有我們自己的類。由于該問題在 4.x 版本中最具有代表性,下面我們將基于 AOSP 4.4 源碼進(jìn)行分析:
▍源碼分析首先從 Daemons 和 FinalizerDaemon 的由來開始分析,Daemons 開始于 Zygote 進(jìn)程:Zygote 創(chuàng)建新進(jìn)程后,通過 ZygoteHooks 類調(diào)用了 Daemons 類的 start() 方法,在 start() 方法中啟動了 FinalizerDaemon,FinalizerWatchdogDaemon 等關(guān)聯(lián)的守護(hù)線程。
Daemons 類主要處理 GC 相關(guān)操作,start() 方法調(diào)用時啟動了 5 個守護(hù)線程,其中有 2 個守護(hù)線程和這個 BUG 具有直接的關(guān)系。
▍FinalizerDaemon 析構(gòu)守護(hù)線程對于重寫了成員函數(shù)finalize()的類,在對象創(chuàng)建時會新建一個 FinalizerReference 對象,這個對象封裝了原對象。當(dāng)原對象沒有被其他對象引用時,這個對象不會被 GC 馬上清除掉,而是被放入 FinalizerReference 的鏈表中。FinalizerDaemon 線程循環(huán)取出鏈表里面的對象,執(zhí)行它們的 finalize() 方法,并且清除和對應(yīng) FinalizerReference對象引用關(guān)系,對應(yīng)的 FinalizerReference 對象在下次執(zhí)行 GC 時就會被清理掉。
析構(gòu)監(jiān)護(hù)守護(hù)線程用來監(jiān)控 FinalizerDaemon 線程的執(zhí)行,采用 Watchdog 計時器機制。當(dāng) FinalizerDaemon 線程開始執(zhí)行對象的 finalize() 方法時,FinalizerWatchdogDaemon 線程會啟動一個計時器,當(dāng)計時器時間到了之后,檢測 FinalizerDaemon 中是否還有正在執(zhí)行 finalize() 的對象。檢測到有對象存在后就視為 finalize() 方法執(zhí)行超時,就會產(chǎn)生 TimeoutException 異常。
由源碼可以看出,該 Crash 是在 FinalizerWatchdogDaemon 的線程中創(chuàng)建了一個TimeoutException 傳給 Thread 類的 defaultUncaughtExceptionHandler 處理造成的。由于異常中填充了 FinalizerDaemon 的堆棧,之所以堆棧中沒有出現(xiàn)和 FinalizerWatchdogDaemon 相關(guān)的類。
finalize()導(dǎo)致的 TimeoutException Crash 非常普遍,很多 APP 都面臨著這個問題。使用 finalize() TimeoutException 為關(guān)鍵詞在搜索引擎或者 Stack Overflow 上能搜到非常多的反饋和提問,技術(shù)網(wǎng)站上對于這個問題的原因分析大概有兩種:
▍對象 finalize() 方法耗時較長當(dāng) finalize() 方法中有耗時操作時,可能會出現(xiàn)方法執(zhí)行超時。耗時操作一般有兩種情況,一是方法內(nèi)部確實有比較耗時的操作,比如 IO 操作,線程休眠等。另外有種線程同步耗時的情況也需要注意:有的對象在執(zhí)行 finalize() 方法時需要線程同步操作,如果長時間拿不到鎖,可能會導(dǎo)致超時,如 android.content.res.AssetManager$AssetInputStream 類:
AssetManager 的內(nèi)部類 AssetInputStream 在執(zhí)行 finalize() 方法時調(diào)用 close() 方法時需要拿到外部類 AssetManager 對象鎖, 而在 AssetManager 類中幾乎所有的方法運行時都需要拿到同樣的鎖,如果 AssetManager 連續(xù)加載了大量資源或者加載資源是耗時較長,就有可能導(dǎo)致內(nèi)部類對象 AssetInputStream 在執(zhí)行finalize() 時長時間拿不到鎖而導(dǎo)致方法執(zhí)行超時。
有種觀點認(rèn)為系統(tǒng)可能會在執(zhí)行 finalize() 方法時進(jìn)入休眠, 然后被喚醒恢復(fù)運行后,會使用現(xiàn)在的時間戳和執(zhí)行 finalize() 之前的時間戳計算耗時,如果休眠時間比較長,就會出現(xiàn) TimeoutException。
詳情請見∞
確實這兩個原因能夠?qū)е?finalize() 方法超時,但是從 Crash 的機型分布上看大部分是發(fā)生在系統(tǒng)類,另外在 5.0 以上版本也有大量出現(xiàn),因此我們認(rèn)為可能也有其他原因?qū)е麓祟悊栴}:
▍IO 負(fù)載過高許多類的 finalize() 都需要釋放 IO 資源,當(dāng) APP 打開的文件數(shù)目過多,或者在多進(jìn)程或多線程并發(fā)讀取磁盤的情況下,隨著并發(fā)數(shù)的增加,磁盤 IO 效率將大大下降,導(dǎo)致 finalize() 方法中的 IO 操作運行緩慢導(dǎo)致超時。
▍FinalizerDaemon 中線程優(yōu)先級過低FinalizerDaemon 中運行的線程是一個守護(hù)線程,該線程優(yōu)先級一般為默認(rèn)級別 (nice=0),其他高優(yōu)先級線程獲得了更多的 CPU 時間,在一些極端情況下高優(yōu)先級線程搶占了大部分 CPU 時間,FinalizerDaemon 線程只能在 CPU 空閑時運行,這種情況也可能會導(dǎo)致超時情況的發(fā)生,(從 Android 8.0 版本開始,FinalizerDaemon 中守護(hù)線程優(yōu)先級已經(jīng)被提高,此類問題已經(jīng)大幅減少)
▍解決方案當(dāng)問題出現(xiàn)后,我們應(yīng)該找到問題的根本原因,從根源上去解決。然而對于這個問題來說卻不太容易實現(xiàn),和其他問題不同,這類問題原因比較復(fù)雜,有系統(tǒng)原因,也有 APP 自身的原因,比較難以定位,也難以系統(tǒng)性解決。
▍理想措施理論上我們可以做的措施有:
1. 減少對 finalize() 方法的依賴,盡量不依靠 finalize() 方法釋放資源,手動處理資源釋放邏輯。
2. 減少 finalizable 對象個數(shù),即減少有 finalize() 方法的對象創(chuàng)建,降低 finalizable 對象 GC 次數(shù)。
3.finalize() 方法內(nèi)盡量減少耗時以及線程同步時間。
4. 減少高優(yōu)先級線程的創(chuàng)建和使用,降低高優(yōu)先級線程的 CPU 使用率。
▍止損措施理想情況下的措施,可以從根本上解決此類問題,但現(xiàn)實情況下卻不太容易完全做到,對一些大型APP來說更難以徹底解決。那么在解決問題的過程中,有沒有別的辦法能夠緩解或止損呢?總結(jié)了技術(shù)網(wǎng)站上現(xiàn)有的方案后,可以總結(jié)為以下幾種:
· 手動修改 finalize() 方法超時時間
理論上我們可以做的措施有:
1. 減少對 finalize() 方法的依賴,盡量不依靠 finalize() 方法釋放資源,手動處理資源釋放邏輯。
2. 減少 finalizable 對象個數(shù),即減少有 finalize() 方法的對象創(chuàng)建,降低 finalizable 對象 GC 次數(shù)。
3.finalize() 方法內(nèi)盡量減少耗時以及線程同步時間。
4. 減少高優(yōu)先級線程的創(chuàng)建和使用,降低高優(yōu)先級線程的 CPU 使用率。
▍止損措施理想情況下的措施,可以從根本上解決此類問題,但現(xiàn)實情況下卻不太容易完全做到,對一些大型APP來說更難以徹底解決。那么在解決問題的過程中,有沒有別的辦法能夠緩解或止損呢?總結(jié)了技術(shù)網(wǎng)站上現(xiàn)有的方案后,可以總結(jié)為以下幾種:
· 手動修改 finalize() 方法超時時間
詳情請見∞
這種方案思路是有效的,但是這種方法卻是無效的。Daemons 類中 的 MAX_FINALIZE_NANOS 是個 long 型的靜態(tài)常量,代碼中出現(xiàn)的 MAX_FINALIZE_NANOS 字段在編譯期就會被編譯器替換成常量,因此運行期修改是不起作用的。MAX_FINALIZE_NANOS默認(rèn)值是 10s,國內(nèi)廠商常常會修改這個值,一般有 15s,30s,60s,120s,我們可以推測廠商修改這個值也是為了加大超時的闕值,從而緩解此類 Crash。
· 手動停掉 FinalizerWatchdogDaemon 線程
詳情請見∞
這種方案利用反射 FinalizerWatchdogDaemon 的 stop() 方法,以使 FinalizerWatchdogDaemon 計時器功能永遠(yuǎn)停止。當(dāng) finalize() 方法出現(xiàn)超時, FinalizerWatchdogDaemon 因為已經(jīng)停止而不會拋出異常。這種方案也存在明顯的缺點:
1. 在 Android 5.1 版本以下系統(tǒng)中,當(dāng) FinalizerDaemon 正在執(zhí)行對象的 finalize() 方法時,調(diào)用 FinalizerWatchdogDaemon 的 stop() 方法,將導(dǎo)致 run() 方法正常邏輯被打斷,錯誤判斷為 finalize() 超時,直接拋出 TimeoutException。
2. Android 9.0 版本開始限制 Private API 調(diào)用,不能再使用反射調(diào)用 Daemons 以及 FinalizerWatchdogDaemon 類方法。
▍終極方案這些方案都是阻止 FinalizerWatchdogDaemon 的正常運行,避免出現(xiàn) Crash,從原理上還是具有可行性的:finalize() 方法雖然超時,但是當(dāng) CPU 資源充裕時,FinalizerDaemon 線程還是可以獲得充足的 CPU 時間,從而獲得了繼續(xù)運行的機會,最大可能的延長了 APP 的存活時間。但是這些方案或多或少都是有缺陷的,那么有其他更好的辦法嗎?
What should we do");
我們的方案就是忽略這個 Crash,那么怎么能夠忽略這個 Crash 呢?首先我們梳理一下這個 Crash 的出現(xiàn)過程:
1. FinalizerDaemon 執(zhí)行對象 finalize() 超時。
2. FinalizerWatchdogDaemon 檢測到超時后,構(gòu)造異常交給 Thread 的 defaultUncaughtExceptionHandler 調(diào)用 uncaughtException() 方法處理。
3. APP 停止運行。
Thread 類的 defaultUncaughtExceptionHandler 我們很熟悉了,Java Crash 捕獲一般都是通過設(shè)置 Thread.setDefaultUncaughtExceptionHandler() 方法設(shè)置一個自定義的 UncaughtExceptionHandler ,處理異常后通過鏈?zhǔn)秸{(diào)用,最后交給系統(tǒng)默認(rèn)的 UncaughtExceptionHandler 去處理,在 Android 中默認(rèn)的 UncaughtExceptionHandler 邏輯如下:
從系統(tǒng)默認(rèn)的 UncaughtExceptionHandler 中可以看出,APP Crash 時彈出的停止運行對話框以及退出進(jìn)程操作都是在這里處理中處理的,那么只要不讓這個代碼繼續(xù)執(zhí)行就可以阻止 APP 停止運行了。基于這個思路可以將這個方案表示為如下的代碼:
這種方案在 FinalizerWatchdogDaemon 出現(xiàn) TimeoutException 時主動忽略這個異常,阻斷 UncaughtExceptionHandler 鏈?zhǔn)秸{(diào)用,使系統(tǒng)默認(rèn)的 UncaughtExceptionHandler 不會被調(diào)用,APP 就不會停止運行而繼續(xù)存活下去。由于這個過程用戶無感知,對用戶無明顯影響,可以最大限度的減少對用戶的影響。
1. 對系統(tǒng)侵入性小,不中斷 FinalizerWatchdogDaemon 的運行。
2.Thread.setDefaultUncaughtExceptionHandler() 方法是公開方法,兼容性比較好,可以適配目前所有 Android 版本。
▍總結(jié)
不管什么樣的緩解措施,都是治標(biāo)不治本,沒有從根源上解決。對于這類問題來說,雖然人為阻止了 Crash,避免了 APP 停止,APP 能夠繼續(xù)運行,但是 finalize() 超時還是客觀存在的,如果 finalize() 一直超時的狀況得不到緩解,將會導(dǎo)致 FinalizerDaemon 中 FinalizerReference 隊列不斷增長,最終出現(xiàn) OOM 。因此還需要從一點一滴做起,優(yōu)化代碼結(jié)構(gòu),培養(yǎng)良好的代碼習(xí)慣,從而徹底解決這個問題。當(dāng)然 BUG 不斷,優(yōu)化不止,在解決問題的路上,緩解止損措施也是非常重要的手段。誰能說能抓老鼠的白貓不是好貓呢?
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 江 義 旺
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 滴滴 | 業(yè)務(wù)平臺技術(shù)
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 資深軟件研發(fā)工程師
曾就職于奇虎360,長期從事移動端研發(fā),2018年加入滴滴,專注于安卓移動端性能優(yōu)化,架構(gòu)演進(jìn),新技術(shù)探索,開源項目DroidAssist 作者。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 看完江義旺同學(xué)的分享
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 你有怎樣的心得體會");? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 請在留言區(qū)告訴我們吧
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/7375.html
摘要:目前本項目僅部分開源已開源內(nèi)容漢字轉(zhuǎn)拼音庫城市列表,索引懸停效果也許是目前最好用的工具類也許是目前最好用的屏幕工具類國際化常用工具類庫成都市詳細(xì)使用請參考倉庫說明。成都市存儲實體對象示例。成都市北京市詳細(xì)使用請參考倉庫說明。 地圖:采用高德地圖,僅簡單完成了部分功能,基礎(chǔ)地圖,地址檢索,逆地理編碼。 界面:仿...
摘要:架構(gòu)都是演變出來的,沒有最好的架構(gòu),只有最合適的架構(gòu)最近,滴滴出行平臺產(chǎn)品中心技術(shù)負(fù)責(zé)人李賢輝接受了的采訪,闡述了滴滴的客戶端架構(gòu)模式與演變過程。李賢輝也是移動開發(fā)精英俱樂部中的一員,所以本期重點推薦了這篇文章。 「架構(gòu)都是演變出來的,沒有最好的架構(gòu),只有最合適的架構(gòu)!」最近,滴滴出行平臺產(chǎn)品中心 iOS 技術(shù)負(fù)責(zé)人李賢輝接受了 infoQ 的采訪,闡述了滴滴的 iOS 客戶端架構(gòu)模式...
閱讀 1552·2021-11-17 09:33
閱讀 1100·2021-11-12 10:36
閱讀 2414·2019-08-30 15:54
閱讀 2441·2019-08-30 13:14
閱讀 2913·2019-08-26 14:05
閱讀 3289·2019-08-26 11:32
閱讀 3001·2019-08-26 10:09
閱讀 2995·2019-08-26 10:09