[Google]-再見-SharedPreferences-擁抱-Jetpack-DataStore

語言: CN / TW / HK

}

複製程式碼

正如你所看到的,開啟一個執行緒非同步讀取資料,當我們正在讀取一個比較大的資料,還沒讀取完,接著呼叫? getXXX() ?方法。

public String getString(String key, @Nullable String defValue) {

synchronized (mLock) {

awaitLoadedLocked();

String v = (String)mMap.get(key);

return v != null ? v : defValue;

}

}

private void awaitLoadedLocked() {

......

while (!mLoaded) {

try {

mLock.wait();

} catch (InterruptedException unused) {

}

}

......

}

複製程式碼

在同步方法內呼叫了? wait() ?方法,會一直等待? getSharedPreferences() ?方法開啟的執行緒讀取完資料才能繼續往下執行,如果讀取幾 KB 的資料還好,假設讀取一個大的檔案,勢必會造成主執行緒阻塞。

SP 不能保證型別安全

呼叫? getXXX() ?方法的時候,可能會出現 ClassCastException 異常,因為使用相同的 key 進行操作的時候, putXXX ?方法可以使用不同型別的資料覆蓋掉相同的 key。

val key = "jetpack"

val sp = getSharedPreferences("ByteCode", Context.MODE_PRIVATE) // 非同步載入 SP 檔案內容

sp.edit { putInt(key, 0) } // 使用 Int 型別的資料覆蓋相同的 key

sp.getString(key, ""); // 使用相同的 key 讀取 Sting 型別的資料

複製程式碼

使用 Int 型別的資料覆蓋掉相同的 key,然後使用相同的 key 讀取 Sting 型別的資料,編譯正常,但是執行會出現以下異常。

java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

複製程式碼

SP 載入的資料會一直留在記憶體中

通過? getSharedPreferences() ?方法載入的資料,最後會將資料儲存在靜態的成員變數中。

// 呼叫 getSharedPreferences 方法,最後會呼叫 getSharedPreferencesCacheLocked 方法

public SharedPreferences getSharedPreferences(File file, int mode) {

......

final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();

return sp;

}

// 通過靜態的 ArrayMap 快取 SP 載入的資料

private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;

// 將資料儲存在 sSharedPrefsCache 中

private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {

......

ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);

if (packagePrefs == null) {

packagePrefs = new ArrayMap<>();

sSharedPrefsCache.put(packageName, packagePrefs);

}

return packagePrefs;

}

複製程式碼

通過靜態的 ArrayMap 快取每一個 SP 檔案,而每個 SP 檔案內容通過 Map 快取鍵值對資料,這樣資料會一直留在記憶體中,浪費記憶體。

apply() ?方法是非同步的,可能會發生 ANR

apply() ?方法是非同步的,為什麼還會造成 ANR 呢?曾今的位元組跳動就出現過這個問題,具體詳情可以點選這裡前去檢視?[剖析 SharedPreference apply 引起的 ANR 問題]( )?而且 Google 也明確指出了? apply() ?的問題。

[圖片上傳中...(image-ab9199-1602236582506-4)]

<figcaption style="display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;"></figcaption>

簡單總結一下: apply() ?方法是非同步的,本身是不會有任何問題,但是當生命週期處於? handleStopService() ?、? handlePauseActivity() ?、? handleStopActivity() ?的時候會一直等待? apply() ?方法將資料儲存成功,否則會一直等待,從而阻塞主執行緒造成 ANR,一起來分析一下為什麼非同步方法還會阻塞主執行緒,先來看看? apply() ?方法的實現。

frameworks/base/core/java/android/app/SharedPreferencesImpl.java

public void apply() {

final long startTime = System.currentTimeMillis();

final MemoryCommitResult mcr = commitToMemory();

final Runnable awaitCommit = new Runnable() {

br/>@Override

public void run() {

mcr.writtenToDiskLatch.await(); // 等待

......

}

};

// 將 awaitCommit 新增到佇列 QueuedWork 中

QueuedWork.addFinisher(awaitCommit);

Runnable postWriteRunnable = new Runnable() {

br/>@Override

public void run() {

awaitCommit.run();

QueuedWork.removeFinisher(awaitCommit);

}

};

// 8.0 之前加入到一個單執行緒的執行緒池中執行

// 8.0 之後加入 HandlerThread 中執行寫入任務

SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

}

複製程式碼

  • 將一個 awaitCommit 的 Runnable 任務,新增到佇列 QueuedWork 中,在 awaitCommit 中會呼叫? await() ?方法等待,在? handleStopService ?、? handleStopActivity ?等等生命週期會以這個作為判斷條件,等待任務執行完畢
  • 將一個 postWriteRunnable 的 Runnable 寫任務,通過? enqueueDiskWrite ?方法,將寫入任務加入到佇列中,而寫入任務在一個執行緒中執行

注意:在 8.0 之前和 8.0 之後? enqueueDiskWrite() ?方法實現邏輯各不相同

在 8.0 之前呼叫? enqueueDiskWrite() ?方法,將寫入任務加入到? 單個執行緒的執行緒池 ?中執行,如果? apply() ?多次的話,任務將會依次執行,效率很低,android-7.0.0_r34 原始碼如下所示。

// android-7.0.0_r34: frameworks/base/core/java/android/app/SharedPreferencesImpl.java

private void enqueueDiskWrite(final MemoryCommitResult mcr,

final Runnable postWriteRunnable) {

......

QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);

}

// android-7.0.0_r34: frameworks/base/core/java/android/app/QueuedWork.java

public static ExecutorService singleThreadExecutor() {

synchronized (QueuedWork.class) {

if (sSingleThreadExecutor == null) {

sSingleThreadExecutor = Executors.newSingleThreadExecutor();

}

return sSingleThreadExecutor;

}

}

複製程式碼

通過? Executors.newSingleThreadExecutor() ?方法建立了一個? 單個執行緒的執行緒池 ,因此任務是序列的,通過? apply() ?方法建立的任務,都會新增到這個執行緒池內。

在 8.0 之後將寫入任務加入到 LinkedList 連結串列中,在 HandlerThread 中執行寫入任務,android-10.0.0_r14 原始碼如下所示。

// android-10.0.0_r14: frameworks/base/core/java/android/app/SharedPreferencesImpl.java

private void enqueueDiskWrite(final MemoryCommitResult mcr,

final Runnable postWriteRunnable) {

......

QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);

}

// android-10.0.0_r14: frameworks/base/core/java/android/app/QueuedWork.java

private static final LinkedList<Runnable> sWork = new LinkedList<>();

public static void queue(Runnable work, boolean shouldDelay) {

Handler handler = getHandler(); // 獲取 handlerThread.getLooper() 生成 Handler 物件

synchronized (sLock) {

sWork.add(work); // 將寫入任務加入到 LinkedList 連結串列中

if (shouldDelay && sCanDelay) {

handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);

} else {

handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);

}

}

}

複製程式碼

在 8.0 之後通過呼叫? handlerThread.getLooper() ?方法生成 Handler,任務都會在 HandlerThread 中執行,所有通過? apply() ?方法建立的任務,都會新增到 LinkedList 連結串列中。

當生命週期處於? handleStopService() ?、? handlePauseActivity() ?、? handleStopActivity() ?的時候會呼叫? QueuedWork.waitToFinish() ?會等待寫入任務執行完畢,我們以其中? handlePauseActivity() ?方法為例。

public void handlePauseActivity(IBinder token, boolean finished, boolean userLeaving,

int configChanges, PendingTransactionActions pendingActions, String reason) {

......

// 確保寫任務都已經完成

QueuedWork.waitToFinish();

......

}

}

複製程式碼

正如你所看到的在? handlePauseActivity() ?方法中,呼叫了? QueuedWork.waitToFinish() ?方法,會等待所有的寫入執行完畢,Google 在 8.0 之後對這個方法做了很大的優化,一起來看一下 8.0 之前和 8.0 之後的區別。

注意:在 8.0 之前和 8.0 之後? waitToFinish() ?方法實現邏輯各不相同

在 8.0 之前? waitToFinish() ?方法只做了一件事,會一直等待寫入任務執行完畢,我先來看看在 android-7.0.0_r34 原始碼實現。

android-7.0.0_r34: frameworks/base/core/java/android/app/QueuedWork.java

private static final ConcurrentLinkedQueue<Runnable> sPendingWorkFinishers =

new ConcurrentLinkedQueue<Runnable>();

public static void waitToFinish() {

Runnable toFinish;

while ((toFinish = sPendingWorkFinishers.poll()) != null) {

toFinish.run(); // 相當於呼叫 mcr.writtenToDiskLatch.await() 方法

}

}

複製程式碼

  • sPendingWorkFinishers ?是 ConcurrentLinkedQueue 例項, apply ?方法會將寫入任務新增到? sPendingWorkFinishers ?佇列中,在? 單個執行緒的執行緒池 ?中執行寫入任務,執行緒的排程並不由程式來控制,也就是說當生命週期切換的時候,任務不一定處於執行狀態

  • toFinish.run() ?方法,相當於呼叫? mcr.writtenToDiskLatch.await() ?方法,會一直等待

  • waitToFinish() ?方法就做了一件事,會一直等待寫入任務執行完畢,其它什麼都不做,當有很多寫入任務,會依次執行,當檔案很大時,效率很低,造成 ANR 就不奇怪了,尤其像位元組跳動這種大規模的 App

在 8.0 之後? waitToFinish() ?方法做了很大的優化,當生命週期切換的時候,會主動觸發任務的執行,而不是一直在等著,我們來看看 android-10.0.0_r14 原始碼實現。

android-10.0.0_r14: frameworks/base/core/java/android/app/QueuedWork.java

private static final LinkedList<Runnable> sFinishers = new LinkedList<>();

public static void waitToFinish() {

......

try {

processPendingWork(); // 主動觸發任務的執行

} finally {

StrictMode.setThreadPolicy(oldPolicy);

}

try {

// 等待任務執行完畢

while (true) {

Runnable finisher;

synchronized (sLock) {

finisher = sFinishers.poll(); // 從 LinkedList 中取出任務

}

if (finisher == null) { // 當 LinkedList 中沒有任務時會跳出迴圈

break;

}

finisher.run(); // 相當於呼叫 mcr.writtenToDiskLatch.await()

}

}

......

}

複製程式碼

在? waitToFinish() ?方法中會主動呼叫? processPendingWork() ?方法觸發任務的執行,在 HandlerThread 中執行寫入任務。

另外還做了一個很重要的優化,當呼叫? apply() ?方法的時候,執行磁碟寫入,都是全量寫入,在 8.0 之前,呼叫 N 次? apply() ?方法,就會執行 N 次磁碟寫入,在 8.0 之後, apply() ?方法呼叫了多次,只會執行最後一次寫入,通過版本號來控制的。

SharedPreferences 的另外一個缺點就是? apply() ?方法無法獲取到操作成功或者失敗的結果,而? commit() ?方法是可以接收 MemoryCommitResult 裡面的一個 boolean 引數作為結果,來看一下它們的方法簽名。

public void apply() { ... }

public boolean commit() { ... }

複製程式碼

SP 不能用於跨程序通訊

我們在建立 SP 例項的時候,需要傳入一個? mode ,如下所示:

val sp = getSharedPreferences("ByteCode", Context.MODE_PRIVATE)

複製程式碼

Context 內部還有一個? mode ?是? MODE_MULTI_PROCESS ,我們來看一下這個? mode ?做了什麼

public SharedPreferences getSharedPreferences(File file, int mode) {

if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||

getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {

// 重新讀取 SP 檔案內容

sp.startReloadIfChangedUnexpectedly();

}

return sp;

}

複製程式碼

在這裡就做了一件事,當遇到? MODE_MULTI_PROCESS ?的時候,會重新讀取 SP 檔案內容,並不能用 SP 來做跨程序通訊。

到這裡關於 SharedPreferences 部分分析完了,接下來分析一下 DataStore 為我們解決什麼問題?

DataStore 解決了什麼問題

Preferences DataStore 主要用來替換 SharedPreferences,Preferences DataStore 解決了 SharedPreferences 帶來的所有問題

Preferences DataStore 相比於 SharedPreferences 優點

  • DataStore 是基於 Flow 實現的,所以保證了在主執行緒的安全性
  • 以事務方式處理更新資料,事務有四大特性(原子性、一致性、 隔離性、永續性)
  • 沒有? apply() ?和? commit() ?等等資料持久的方法
  • 自動完成 SharedPreferences 遷移到 DataStore,保證資料一致性,不會造成資料損壞
  • 可以監聽到操作成功或者失敗結果

另外 Jetpack DataStore 提供了 Proto DataStore 方式,用於儲存類的物件(typed objects ),通過 protocol buffers 將物件序列化儲存在本地,protocol buffers 現在已經應用的非常廣泛,無論是微信還是阿里等等大廠都在使用,我們在部分場景中也使用了 protocol buffers,在後續的文章會詳細的分析。

注意:

Preferences DataStore 只支援? Int ?,? Long ?,? Boolean ?,? Float ?,? String ?鍵值對資料,適合儲存簡單、小型的資料,並且 不支援區域性更新 ,如果修改了其中一個值,整個檔案內容將會被重新序列化,可以執行?[AndroidX-Jetpack-Practice/DataStoreSimple]( )?體驗一下,如果需要區域性更新,建議使用 Room。

在專案中使用 Preferences DataStore

Preferences DataStore 主要應用在 MVVM 當中的 Repository 層,在專案中使用 Preferences DataStore 非常簡單,只需要 4 步。

1. 需要新增 Preferences DataStore 依賴

implementation "androidx.datastore:datastore-preferences:1.0.0-alpha01"

複製程式碼

2. 構建 DataStore

private val PREFERENCE_NAME = "DataStore"

var dataStore: DataStore<Preferences> = context.createDataStore(

name = PREFERENCE_NAME

複製程式碼

總結

Android架構學習進階是一條漫長而艱苦的道路,不能靠一時激情,更不是熬幾天幾夜就能學好的,必須養成平時努力學習的習慣。 所以:貴在堅持!

上面分享的位元組跳動公司2021年的面試真題解析大全,筆者還把一線網際網路企業主流面試技術要點整理成了影片和PDF(實際上比預期多花了不少精力),包含知識脈絡 + 諸多細節。

Android學習PDF+學習影片+面試文件+知識點筆記

【Android高階架構影片學習資源】

Android部分精講影片領取學習後更加是如虎添翼!進軍BATJ大廠等(備戰)!現在都說網際網路寒冬,其實無非就是你上錯了車,且穿的少(技能),要是你上對車,自身技術能力夠強,公司換掉的代價大,怎麼可能會被裁掉,都是淘汰末端的業務Curd而已!現如今市場上初級程式設計師氾濫,這套教程針對Android開發工程師1-6年的人員、正處於瓶頸期,想要年後突破自己漲薪的,進階Android中高階、架構師對你更是如魚得水,趕快領取吧!