專案中多次操作SharedPreferences導致ANR場景的解決

效能優化實踐者發表於2021-11-12

專案背景:

隨著時代的進步,移動端廣告的投放變得越來越多樣化,為了接近市場,不少公司自己研發了SDK去收集使用者的一些資訊以及行為用於分析,根據分析結果使用自定義廣告(自定義View)的方式繼續向使用者進行展示,以提高展示率和點選率。
目前關於廣告商方面的選擇,國內的廣告變現普遍較低,首選應該是接入谷歌廣告。隨著業務的發展,在一段時間後,公司開始轉變成廣告接收方,並靠自己的SDK來進行廣告的投放,以及優化。
以定位來獲取廣告的方式為例:
· 首先利用使用者的定位等許可權來獲取經緯度


· 將經緯度上傳至國內某定位SDK,獲取具體資訊。

· 最後根據定位資訊來獲取廣告並預載入為廣告展示做準備。

所遇到的挑戰:

在專案功能完成後準備上架前,會對專案進行一系列的測試,但是ANR問題在測試過程中很難完成復現,在使用多個機型測試的過程中幾乎沒有ANR問題的記錄。但是在使用者的實際使用過程中,由於Android碎片化的嚴重,加上使用者的一些操作的習慣等,會導致出現ANR的問題。面對一些大型的廠家,ANR出現時會彈窗引導使用者關閉軟體,會導致使用體驗不好,造成使用者的流失。

解決問題的步驟:
在專案一個多月的異常收集中,出現過幾次ANR的問題。
· 分析異常收集中ANR日誌將問題鎖定在SharedPreferences 上,排查過程比較麻煩,在鎖定了問題後,開始對問題進行分析。
· 檢視Android文件:在專案中,團隊使用SharedPreferences讀寫配置檔案,均採用了官方的推薦做法,呼叫apply來提交,呼叫這個方法時,先寫入記憶體中,再將任務加入佇列中,會在非同步執行緒中做落盤的操作,這個操作理論上來說是沒有問題的,也是google官方推薦的做法。
· 閱讀原始碼:

在此過程中發現谷歌官方註釋:
If another editor on this SharedPreferences does a regular commit() while a apply() is still outstanding, the commit() will block until all async commits are completed as well as the commit itself.
翻譯:如果SharedPreferences上的另一個編輯器執行常規的commit(),而apply()仍然未完成,則commit()將阻塞,直到所有非同步提交以及提交本身都完成。

· 鎖定問題:主執行緒呼叫了 QueuedWork.waitToFinish(),沒有待執行的任務,直接執行 finisher,進行阻塞等待, 直到寫入檔案成功後恢復執行, 這時候如果等待時間過長, 在一些市面上效能差的中低端機型上就會很容易出現ANR。 (8.0以下)
問題的解決
當時的優化:

  1. 減小sp對應的檔案的大小,按照分類去讀寫對應的sp檔案。
  2. sp的讀寫輕量的、小的配置資訊,將類似JSON的資料交給其他方式儲存。
  3. 當需要多次呼叫Put系列方法,當邏輯確認不需要立即讀取時,在最後一次呼叫commit或apply即可。

最近朋友推了一篇位元組的部落格(以下文字以及圖片來源於位元組今日頭條團隊)。

· 思路:如果能讓sPendingWorkFinishers.poll()返回為null,則這裡的等待行為直接就跳過去了,sPendingWorkFinishers是個ConcurrentLinkedQueue集合,可以直接動態代理這個集合,覆寫poll方法,讓其永遠返回null,這個時候UI永遠不會等待子執行緒寫入檔案完畢,事實證明這種方式簡單有效。

· 針對這種寫入等待的ANR問題,還有一種就是全域性替換寫入方式,通過插樁的方式,替換所有的API實現,採用其他的儲存方式,這種方式修復成本和風險較大,但是後期可以隨機的替換儲存方式,使用比較靈活。

友盟平臺相關SDK初體驗:
由於ANR的比較難復現,於是寫一個方法,反覆對SharedPreferences進行操作,以達到類似情況的復現。

出現問題,通過友盟U-APM平臺定位:

找到問題後,進行文中思路的操作即可。

總結
在情景中,由於Android太過碎片化,又不得直接捨棄低版本使用者,採用接入類似友盟U-APM平臺的方式去更快的解決問題是必不可少的。但對於一些小型手機的低版本可能還是會出現ANR的問題,針對類似收集使用者行為的情景,可採取進行多種方式去進行收集,例如對於低版本的系統,降低對收集資料的完整性等。

作者:計蒙不吃魚

相關文章