剖析 SharedPreference apply 引起的 ANR 問題

項目中 ANR 率居高不下,從統計上來看排在前面的有幾個都是 SharedPreference(以下簡稱 SP)引起的。接下來我們抽絲剝繭的來分析其產生原因及如何解決。

crash 堆棧信息如下。從 crash 收集平臺上來看,有幾個類似的堆棧信息。唯一的區別就是 ActivityThread 的入口方法。除了 ActivityThread 的 handleSleeping 方法之外,還有 handleServiceArgs、handleStopService、handleStopActivity。

剖析 SharedPreference apply 引起的 ANR 問題

ActivityThread 的這幾個方法是 Activity 或 Service 的生命週期變化的時候調用的。從堆棧信息來看,組件生命週期變化,導致調用 QueueWork 中的隊列處於等待狀態,等待超時則發生 ANR。那麼 QueuedWork 的工作機制是什麼樣的呢,我們從源碼入手來進行分析。

SP 的 apply 到底做了什麼

首先從問題的源頭開始,SP 的 apply 方法。

剖析 SharedPreference apply 引起的 ANR 問題

apply 方法,首先創建了一個 awaitCommit 的 Runnable,然後加入到 QueuedWork 中,awaitCommit 中包含了一個等待鎖,需要在其它地方釋放。我們在上面看到的 QueuedWork.waitToFinish() 其實就是等待這個隊列中的 awaitCommit 全部釋放。

然後通過 SharedPreferencesImpl.this.enqueueDiskWrite 創建了一個任務來執行真正的 SP 持久化。

剖析 SharedPreference apply 引起的 ANR 問題

其實無論是 SP 的 commit 還是 apply 最終都會調用 enqueueDiskWrite 方法,區別是 commit 方法調用傳遞的第二個參數為 null。此方法內部也是根據第二個參數來區分 commit 和 apply 的,如果是 commit 則會同步的執行 writeToFile,apply 則會將 writeToFile 加入到一個任務隊列中異步的執行,從這裡也可以看出 commit 和 apply 的真正區別。

writeToFile 執行完成會釋放等待鎖,之後會回調傳遞進來的第二個參數 Runnable 的 run 方法,並將 QueuedWork 中的這個等待任務移除。

總結來看,SP 調用 apply 方法,會創建一個等待鎖放到 QueuedWork 中,並將真正數據持久化封裝成一個任務放到異步隊列中執行,任務執行結束會釋放鎖。Activity onStop 以及 Service 處理 onStop,onStartCommand 時,執行 QueuedWork.waitToFinish() 等待所有的等待鎖釋放。

如何解決,清空等待隊列

從上述分析來看,SP 操作僅僅把 commit 替換為 apply 不是萬能的,apply 調用次數過多容易引起 ANR。所有此類 ANR 都是經由 QueuedWork.waitToFinish() 觸發的,如果在調用此函數之前,將其中保存的隊列手動清空,那麼是不是能解決問題呢,答案是肯定的。

Activity 的 onStop,以及 Service 的 onStop 和 onStartCommand 都是通過 ActivityThread 觸發的,ActivityThread 中有一個 Handler 變量,我們通過 Hook 拿到此變量,給此 Handler 設置一個 callback,Handler 的 dispatchMessage 中會先處理 callback。

剖析 SharedPreference apply 引起的 ANR 問題

在 Callback 中調用隊列的清理工作

剖析 SharedPreference apply 引起的 ANR 問題

隊列清理需要反射調用 QueuedWork。

剖析 SharedPreference apply 引起的 ANR 問題

清理等待鎖會產生什麼問題

SP 無論是 commit 還是 apply 都會產生 ANR,但從 Android 之初到目前 Android8.0,Google 一直沒有修復此 bug,我們貿然處理會產生什麼問題呢。Google 在 Activity 和 Service 調用 onStop 之前阻塞主線程來處理 SP,我們能猜到的唯一原因是儘可能的保證數據的持久化。因為如果在運行過程中產生了 crash,也會導致 SP 未持久化,持久化本身是 IO 操作,也會失敗。我們清理了等待鎖隊列,會對數據持久化造成什麼影響呢,下面我們通過一組實驗來驗證。

進程啟動的時候,產生一個隨機數字。用 commit 和 apply 兩種方式來存此變量。第二次進程啟動,獲取以兩種方式存取的值並做比較,如果相同表示 apply 持久化成功,如果不相同表示 apply 持久化失敗。

實驗一:開啟等待鎖隊列的清理。

實驗二:關閉等待鎖隊列的清理。

線上同時開啟兩個實驗,在實驗規模相同的情況下,統計 apply 失敗率。

實驗一,失敗率為 1.84%

實驗二,失敗率為為 1.79%

可見,apply 機制本身的失敗率就比較高,清理等待鎖隊列對持久化造成的影響不大。

目前頭條 app 已經全量開啟清理等待鎖策略,上線至今沒有發現此策略產生的用戶反饋。


分享到:


相關文章: