今日頭條 ANR 優化實踐系列 - 告別 SharedPreference 等待

簡述

前面系列文章中介紹了安卓系統ANR設計原理以及咱們在實際工做中對ANR進行監控獲得的一些方案,基於這些常規的監控治理,ANR問題獲得了有效的抑制。可是有些系統組件的設計初衷與開發人員在實際使用過程當中的背離,致使的問題亟待解決。當前文章針對實際開發過程當中濫用sp致使的ANR問題,如何從系統層面跳過Google設計缺陷,規避ANR問題。markdown

Google在設計之初爲了方便開發者,實現了一套輕量級的數據持久化方案——SharedPreference(如下簡稱sp),由於其簡便的API,方便的使用方式,獲得開發者的青睞,對其依賴愈來愈重。在應用版本不斷迭代過程當中發現Google說的輕量級的數據存儲是有緣由的,越是重量級的應用出現的ANR問題越嚴重。本文從源碼層面分析sp文件的加載解析和寫入過程出發,分析致使ANR問題的緣由分析以及相關的優化解決方案。架構

SP致使ANR緣由分析

常常會遇到兩類關於SharedPreference問題,如下分別介紹致使這兩類ANR問題的緣由和優化方案。app

問題一:sp建立之後,會單獨的使用一個線程來加載解析對應的sp文件。可是當UI線程嘗試訪問sp中內容時,若是sp文件還未被徹底加載解析到內存,此時UI線程會被block,直到sp文件被徹底加載到內存中爲止。具體ANR線程堆棧以下:異步

主要緣由是sp文件未被加載或解析到內存中,此時沒法直接使用sp提供的接口。sp被建立的時候會同時啓動一個線程加載對應的sp文件,執行startLoadFromDisk();工具

在startLoadFromDisk()時,標記sp不可以使用狀態,後期不管是嘗試讀數據或者寫數據,讀寫線程都會被直接block,直到sp文件被所有加載解析完畢。oop

線程在讀或寫時,都會走到awaitLoadedLocked()邏輯,在上圖的mLoaded爲false即sp文件還沒有加載解析到內存,此時讀寫線程會直接被block在mLock鎖上,直到loadFromDisk()方法執行完畢。性能

sp文件徹底加載解析到內存中,直接喚起全部在等待在當前sp的讀寫線程。優化

問題二:Google系統爲了確保數據的跨進程完整性,前期應用可使用sp來作跨進程通訊,在組件銷燬或其餘生命週期的同時爲了確保當前這個寫入任務必須在當前這個組件的生命週期完成寫入,此時主線程會在組件銷燬或者組件暫停的生命週期內等待sp徹底寫入到對應的文件中,以下圖UI線程被block在了QueuedWork.waitToFinish()處,接下來基於源碼從apply開始到最後寫入文件總體流程梳理找出問題根源。spa

具體須要等待文件寫入的消息在AcitivtyThread的H中,具體消息類型以下:線程

public static final int SERVICE_ARGS = 115;
public static final int STOP_SERVICE = 116;
public static final int PAUSE_ACTIVITY = 101;
public static final int STOP_ACTIVITY_SHOW = 103;
public static final int SLEEPING  = 137;
複製代碼

因爲Google官方設計之初是輕量級的數據存儲方式,這種等待行爲不會有什麼問題,可是實際使用過程當中因爲sp過分使用,這個等待的時間被不可控的拉長,直到最後出現ANR,這種問題越在業務繁重的應用上體現越明顯。具體問題堆棧以下,全是系統堆棧,接下來從waitToFinish入手分析,剖析這個ANR的根源。具體ANR堆棧以下:

前期sp接口只有commit接口,接口同步寫入文件,這個接口直接影響開發者使用,因而Google官方對外提供了異步的apply接口,因爲開發者認爲這個異步是真正意義上的異步,大規模的使用sp的appy接口,就是這種apply的實現方式致使了業務量大的APP深受這個apply設計缺陷致使的ANR問題影響。

apply接口總體的詳細設計思路以下圖(基於Android8.0及如下版本分析):

總體的思路簡單梳理以下:

  1. sp.apply(),寫入內存同時獲得須要同步寫入文件的數據集合MemoryCommitResult:

  1. 將MemoryCommitResult封裝成Runnable拋到子線程queued-work-looper中;

  2. 在子線程中將MemoryCommitResult中的mapToWriteToDisk對應的key-value寫入到文件中去;

  3. 文件寫入完成之後,會執行MemoryCommitResult的setDiskWriteResult方法,關鍵的步驟writtenToDiskLatch.countDown() 出現了;

  4. 以下當主線中執行到QueuedWork.waitToFinish()的時候;

  1. 主線程到底在幹什麼,這個時候得從QueuedWork.add(Runnable finisher)入手,具體Runnable以下圖,這個地方就是啥也沒幹,直接等在了mcr.writtenToDiskLatch.await()上,這裏你們應該有點印象,就是步驟4中子線程在寫完文件之後直接釋放的那個鎖

結論:儘管總體API的流程分析異常的複雜,把一個runnable封裝了一層又一層,從這個線程拋到那個線程,子線程執行完寫入文件之後會釋放鎖,主線程執行到某些地方得等待子線程把寫入文件的行爲執行完畢,可是總體的思路仍是比較簡單的。形成這個問題的根源就是太多pending的apply行爲沒有寫入到文件,主線程在執行到指定消息的時候會有等待行爲,等待時間過長就會出現ANR。

儘管Google官方在Android8.0及之後版本對sp寫入邏輯進行優化,指望是在上述步驟6中UI線程不是傻傻的等,而是幫助子線程一塊兒寫入,可是因爲是主線程保守協助,並無很好的解決這個問題。

解決方案

問題一:針對加載很慢的問題,通常使用的比較多的是採用預加載的方式來觸發到這個sp文件的加載和解析,這樣在真正使用的時候大機率sp已經加載解析完畢了;真正須要處理的是核心場景的sp必定不能太大,Google官方的聲明仍是有必要遵照一下,輕量級的數據持久化存儲方式,不要存太多數據,避免文件過大,致使前期的加載解析耗時太久。

問題二:至於Google爲何要這麼設計,提出了本身的幾個猜測:

  1. Google但願作到數據可能儘量及時的寫入文件,可是這樣等待沒有任何意義,主線程直接等待並不會提高寫入的效率;

  2. 指望sp實時寫入文件,以方便跨進程的時候能夠實時的訪問到sp內的文件,這種異步寫入方式自己就沒辦法確保實時性;

  3. 多是在組件發生狀態切換的時候,這個時候若是進程內沒有啥組件,進程的優先級可能下降,存在進程會在系統資源吃緊的時候被系統幹掉,這種機率極低,幾乎能夠忽略不計;

  4. 感受最大的可能性就是Google官方當時是爲了從commit無縫的切換到apply,依然模擬原來的commit行爲,只是將原來的每次寫入文件一次改爲屢次commit行爲最後一次性apply在主線程等待全部的寫入行爲一次性所有寫入。

經過以上假設,發現這裏的主線程等待子線程寫入根本沒有什麼意義,所以但願能夠經過一些必要的手段跳過這種無用的等待行爲,在研究了全部的SharedPreference相關的邏輯後找到如下入手點。如下是8.0如下版本的優化策略,8.0及以上版本處理方式相似:

若是須要主線程在waitToFinish的時候直接跳過去,讓toTinish.run()不執行,顯然不可能,若是能讓sPendingWorkFinishers.poll()返回爲null,則這裏的等待行爲直接就跳過去了,sPendingWorkFinishers是個ConcurrentLinkedQueue集合,能夠直接動態代理這個集合,覆寫poll方法,讓其永遠返回null,這個時候UI永遠不會等待子線程寫入文件完畢,事實證實這種方式簡單有效。

針對這種寫入等待的ANR問題,還有一種就是全局替換寫入方式,經過插樁的方式,替換全部的API實現,採用其餘的存儲方式,這種方式修復成本和風險較大,可是後期能夠隨機的替換存儲方式,使用比較靈活。

方案收益

經過在字節系多個產品的驗證,方案穩定有效,相應堆棧致使的ANR問題消滅殆盡,ANR收益明顯,相應的界面跳轉等場景流暢性獲得了明顯的改善。

展望

Google 新增長了一個新 Jetpack 的成員 DataStore,將來可能用來替換 SharedPreferences, DataStore 應該是開發者期待已久的庫,DataStore 是基於 Flow 實現的,一種新的數據存儲方案。詳細介紹網上有不少參考資料。

優化實踐更多參考

今日頭條ANR 優化實踐系列(1)-設計原理及影響因素

今日頭條ANR 優化實踐系列(2)-監控工具與分析思路

今日頭條ANR 優化實踐系列(3)-實例剖析集錦

今日頭條ANR 優化實踐系列(4)- Barrier 致使主線程假死

Android 平臺架構團隊

咱們是字節跳動 Android 平臺架構團隊,以服務今日頭條爲主,面向 GIP,同時服務公司其餘產品,在產品性能穩定性等用戶體驗,研發流程,架構方向上持續優化和探索,知足產品快速迭代的同時,追求極致的用戶體驗。

若是你對技術充滿熱情,想要迎接更大的挑戰和舞臺,歡迎加入咱們,北京,深圳均有崗位,感興趣發送郵箱:tech@bytedance.com ,郵件標題:姓名 - GIP - Android 平臺架構。


歡迎關注 「字節跳動技術團隊」

相關文章
相關標籤/搜索