Android 疑難雜症修復之路(二)- SharedPreferences ANR

Image source: https://androidcommunity.com/android-p-will-crash-unresponsive-apps-wont-show-those-anr-dialogs-20180515/

上一篇文解決了 Google Play 後台中前幾名的 RemoteServiceException Crash,接著預計要大致解決排名前幾的 ANR,也就是因為使用了 SharedPreferences 之後導致的 ANR。

SharedPreferences 導致的 ANR 通常的 stack trace 會是類似這樣:

  at java.lang.Object.wait! (Native method)
  at java.lang.Thread.parkFor$ (Thread.java:1220)
  at sun.misc.Unsafe.park (Unsafe.java:299)
  at java.util.concurrent.locks.LockSupport.park (LockSupport.java:158)
  at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt (AbstractQueuedSynchronizer.java:810)
  at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireSharedInterruptibly (AbstractQueuedSynchronizer.java:970)
  at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireSharedInterruptibly (AbstractQueuedSynchronizer.java:1278)
  at java.util.concurrent.CountDownLatch.await (CountDownLatch.java:203)
  at android.app.SharedPreferencesImpl$EditorImpl$1.run (SharedPreferencesImpl.java:366)
  at android.app.QueuedWork.waitToFinish (QueuedWork.java:88)
  at android.app.ActivityThread.handleServiceArgs (ActivityThread.java:4134)
  at android.app.ActivityThread.access$2400 (ActivityThread.java:229)
  at android.app.ActivityThread$H.handleMessage (ActivityThread.java:1924)
  at android.os.Handler.dispatchMessage (Handler.java:102)
  at android.os.Looper.loop (Looper.java:148)
  at android.app.ActivityThread.main (ActivityThread.java:7325)
  at java.lang.reflect.Method.invoke! (Native method)
  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run (ZygoteInit.java:1230)
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1120)

這個問題的主要原因通常是因為在 App 內大量使用 SharedPreferences 的 apply,之後再遇到一些 Activity 或是 Service 的操作後就會卡在這裡導致 ANR。網路上其實有很多從 source code 分析這問題成因的文章,這裡我們就省略 source code 直接把總結的原因歸納如下:

  1. apply 是一個 async 方法,但在把數據寫入 xml 前需要先確保 SharedPreferences 的操作都是依序寫入的
  2. SharedPreferences 用來確保的方法是用一個 CountDownLatch 來鎖住 operation,並透過 QueueWork 來把 queue 這些實作 lock 的 runnable
  3. 當遇到任何 Service Start / Stop 或是 Activity Pause / Stop / Resume 的操作時,ActivityThread 就會嘗試等待所有 QueueWork 完成
  4. 不幸的是 QueueWork 在 Android 8 之前是單一 thread 的 theadpool,所以當 QueueWork 放了一堆等待 IO 操作的 task lock 時,在低階手機上就很有可能操過 ANR 的判斷門檻 (UI 操作 5s,Service 操作 20s,Receiver 是 10s)

不過實際上最主要的原因是因為 Android 的實作有問題,在 Android 8 之後 Google 就已經修正了這個問題。主要的修正是透過對 SharedPreference 加上版本標記,避免對每個 Apply 操作都寫檔案,只要對最後一次的操作寫入即可。另一個是改寫了 QueueWork 的實作直接觸發 pending task 而不是只有等待 task。

從 App Developer 的角度來說要解決這個問題有幾個:

  1. Override Application 的 getSharedPreferences 給一個自己實作的 SharedPreferences 僅把 apply 改為自己開 thread 並調用 commit,其他操作都轉發給原本 SharedPreferences instance。
  2. 在特定 ActivityThead 的 message handler 中把 QueueWork 中的內容直接清除
  3. 不要使用 SharedPreferences,有各種開源基於 SQLite 的 SharedPreferences 替代方案。現在則是可以選用 JetPack 的 DataStore

最後如果我們還是提供無腦的解決方案,透過我們提供的 Android Vital Fix library 直接繼承 VitalFixApplication 或是透過 VitalFix.Builder 一行 code 直接 fix (直接採用將 QueueWork 的 list 清除的方法)。


Android 疑難雜症修復之路系列
(一) RemoteServiceException



Leave a Reply

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *