Android Jetpack - DataStore 指南
介紹:
在官方尚未出手之前,儲存鍵值對等小型資料集可能普遍採用兩種方式,SharedPreferences或是MMKV(如果您需要支援大型或複雜資料集、部分更新或參照完整性,請考慮使用 Room,而不是 DataStore。DataStore 非常適合簡單的小型資料集,不支援部分更新或參照完整性。)
MMKV這次暫時不提及,因為DatStore本身對比的也就是SharedPreferences,而且官方也是明確的建議我們遷移到DataStore。
DataStore包含了兩種實現方式:
- Preferences DataStore
僅使用鍵儲存和訪問值資料。此實現不需要預定義的架構,並且不提供型別安全性。
- Proto DataStore
將資料儲存為自定義資料型別的例項。此實現要求您使用協議緩衝區(protobuf - PB協議)定義架構,但它提供型別安全性。
與SharedPreferences的對比:
首先我們來看官方的一張對比
功能 | SharedPreferences | PreferencesDataStore | ProtoDataStore |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | -------------------------------------------------------------------- |
| 非同步 API | ✅(僅用於通過監聽器讀取已更改的值) | ✅(通過 Flow
以及 RxJava 2 和 3 Flowable
) | ✅(通過 Flow
以及 RxJava 2 和 3 Flowable
) |
| 同步 API | ✅(但無法在介面執行緒上安全呼叫) | ❌ | ❌ |
| 可在介面執行緒上安全呼叫 | ❌ | ✅(這項工作已在後臺移至 Dispatchers.IO
) | ✅(這項工作已在後臺移至 Dispatchers.IO
) |
| 可以提示錯誤 | ❌ | ✅ | ✅ |
| 不受執行時異常影響 | ❌ | ✅ | ✅ |
| 包含一個具有強一致性保證的事務性 API | ❌ | ✅ | ✅ |
| 處理資料遷移 | ❌ | ✅ | ✅ |
| 型別安全 | ❌ | ❌ | ✅ 使用協議緩衝區
我們先暫時只看PreferencesDataStore和SharedPreferences
首先同步API和非同步API這兩點區別是沒有問題的。
SharedPreferences:
- apply()
來完成非同步操作:會立即更改記憶體中的 SharedPreferences
物件,但會將更新非同步寫入磁碟。而且apply()還有個問題就是,雖然他本身是非同步的來完成IO操作,但是在SharedPreferencesImpl.EditorImpl.apply()中會新增到QueuedWork
中,當Service或者Activity啟動或停止時,具體可見ActivityThread中handleServiceArgs
,handleStopService
,handlePauseActivity
,handleStopActivity
均會執行QueuedWork.waitToFinish()
等待資料寫入的完成,因為要保證資料不會丟失,但是我們也知道,onPause() 是不適合執行耗時操作的,因為當你期待另一個Activity的時候,會先onPause當前Activity,這很明顯,假如你寫入了較多內容,然後立馬啟動了另一個Activity,結果在onPause()被阻塞,就很容易導致ANR。
- commit()
來實現同步操作,但應避免從主執行緒呼叫它,因為它可能會阻塞UI執行緒,這點沒什麼好說的,而且會返回Boolean值來表示寫入是否成功。
詳細的可以檢視SharedPreferences.Editor介面提供的註釋,具體的實現在SharedPreferencesImpl.EditorImpl我這裡就不貼原始碼了。
回到DataStore,PreferencesDataStore本身是基於攜程Flow來實現的,所以非同步API這點沒有任何問題,不過至於同步的使用方式,放到後面來說,我們先看普遍的非同步使用方式。
可以參考官方出的教學:http://developer.android.com/codelabs/android-preferences-datastore#0
我就不一一複述了。
使用:
````kotlin private const val USER_PREFERENCES_NAME = "user_preferences"
private val Context.dataStore by preferencesDataStore(
name = USER_PREFERENCES_NAME
)
`
首先是通過委託拿到
DataStore
````kotlin
public fun preferencesDataStore(
name: String,
corruptionHandler: ReplaceFileCorruptionHandler
internal class PreferenceDataStoreSingletonDelegate internal constructor(
private val name: String,
private val corruptionHandler: ReplaceFileCorruptionHandler
private val lock = Any()
@GuardedBy("lock")
@Volatile
private var INSTANCE: DataStore<Preferences>? = null
override fun getValue(thisRef: Context, property: KProperty<*>): DataStore<Preferences> {
return INSTANCE ?: synchronized(lock) {
if (INSTANCE == null) {
val applicationContext = thisRef.applicationContext
INSTANCE = PreferenceDataStoreFactory.create(
corruptionHandler = corruptionHandler,
migrations = produceMigrations(applicationContext),
scope = scope
) {
applicationContext.preferencesDataStoreFile(name)
}
}
INSTANCE!!
}
}
}
本質上還是走的
PreferenceDataStoreFactory.create()```建立,是一個非常標準的雙重檢查鎖單例。
來看一下create()函式的引數吧
- corruptionHandler: 異常處理,當反序列化錯誤時會走到這,可以用於讀取錯誤是返回預設值或捕獲異常。
- migrations: 用於遷移SharedPreferences到PreferenceDataStore
- scope: 協程的作用域,指定IO操作及資料轉換的執行的協程作用域
- produceFile: 基於提供的Context和name建立或讀取對應的檔案,預設路徑為
this.applicationContext.filesDir+
datastore/fileName`
推薦做法也是通過PreferenceDataStoreFactory來建立DataStore例項並作為單例注入需要它的類中。
讀取:
dataStore.data
.catch { exception ->
// 有異常丟擲
if (exception is IOException) {
// 使用預設空值
emit(emptyPreferences())
} else {
// 其他異常則繼續丟擲
throw exception
}
}.map { preferences ->
// 資料轉化
}.collect {
// 收集資料
}
emptyPreferences()可以參考上面提到的官方教學示例,裡面會詳細介紹PreferenceData的KV
可以看出dataStore返回的是一個Flow,你可以很方便的轉換成你所需要的資料。
對於Java則是返回的Flowable,和Flow差別不大。
寫入:
``` val INT_KEY = intPreferencesKey("int_key") dataStore.edit { preferences -> preferences[INT_KEY] = 1 }
// 呼叫
public suspend fun DataStore
再回到上圖中,DataStore 的主要優勢之一是非同步API,所以本身並未提供同步API呼叫,但實際上可能不一定始終能將周圍的程式碼更改為非同步程式碼。如果您使用的現有程式碼庫採用同步磁碟 I/O,或者您的依賴項不提供非同步API,就可能出現這種情況。
這個時候可以採用協程本身提供 runBlocking()
以幫助消除同步與非同步程式碼之間的差異。您可以使用 runBlocking()
從 DataStore 同步讀取資料。RxJava 在 Flowable
上提供阻塞方法。會阻塞發起呼叫的執行緒,直到 DataStore 返回資料。
kotlin
runBlocking { context.dataStore.data.first() }
runBlocking()會執行一個新的協程並阻塞當前執行緒直到內部邏輯完成,所以儘量避免在UI執行緒呼叫。
而且還需要注意的一點是,不用在初始讀取時呼叫runBlocking,會阻塞當前執行的執行緒,因為初始讀取會有較多的IO操作,耗時較長。
更為推薦的做法則是先非同步讀取到記憶體後,後續有需要可直接從記憶體中拿,而非運行同步程式碼阻塞式獲取。
總結:
總的來說還是很推薦使用DataStore,與協程的搭配,用起來也是非常的便利,至於PB協議的ProtoDataStore,可以參考官方的示例來實踐,差別主要是還是集中在PB檔案的處理。