概念
- 輕量級數(shù)據(jù)存儲方案
- Kotlin Countinue+Flow 以異步,一致的事務(wù)方式存儲數(shù)據(jù)
- SharedPrefderences方案的替代者
-
Sp的痛點
詳情參見再見 SharedPreferences 擁抱 Jetpack DataStore- getXXX可能會阻塞主線程:在同步方法內(nèi)調(diào)用了 wait() 方法,會一直等待 getSharedPreferences() 方法開啟的線程讀取完數(shù)據(jù)才能繼續(xù)往下執(zhí)行
- 類型不一定安全:相同的key,putInt(key,0),getString(key),就會出現(xiàn)ClassCastException異常
- Sp加載的數(shù)據(jù)會一直存在內(nèi)存中:通過靜態(tài)的 ArrayMap 緩存每一個 SP 文件,而每個 SP 文件內(nèi)容通過 Map 緩存鍵值對數(shù)據(jù),這樣數(shù)據(jù)會一直留在內(nèi)存中,浪費內(nèi)存。
- apply方法是異步的,但可能會導(dǎo)致ANR:當生命周期處于 handleStopService() 、 handlePauseActivity() 、 handleStopActivity() 的時候會一直等待 apply() 方法將數(shù)據(jù)保存成功,否則會一直等待,從而阻塞主線程造成 ANR
- SP 不能用于跨進程通信:當遇到 MODE_MULTI_PROCESS 的時候,會重新讀取 SP 文件內(nèi)容,并不能用 SP 來做跨進程通信。
-
DataStore的優(yōu)勢
- DataStore 是基于 Flow 實現(xiàn)的,所以保證了在主線程的安全性
- 以事務(wù)方式處理更新數(shù)據(jù),事務(wù)有四大特性(原子性、一致性、 隔離性、持久性)
- 沒有 apply() 和 commit() 等等數(shù)據(jù)持久的方法
- 自動完成 SharedPreferences 遷移到 DataStore,保證數(shù)據(jù)一致性,不會造成數(shù)據(jù)損壞
- 可以監(jiān)聽到操作成功或者失敗結(jié)果
-
- Preferences DataStore (鍵值對) 方式
- Preference DataStore 本質(zhì)也是proto buffer存儲,只是這個proto文件時框架自己提供的,對應(yīng)的Serializer為PreferencesSerializer,proto大致如下:
syntax = "proto2"; ...... message PreferenceMap { map<string, Value> preferences = 1; } message Value { oneof valueName { bool boolean = 1; float float = 2; int32 integer = 3; int64 long = 4; string string = 5; double double = 7; } }
- Proto DataStore方式
- proto文件可完全自定義,類型更加靈活
- 序列化:對象->可存儲傳輸?shù)淖止?jié)序列;反序列化倒過來
- 數(shù)據(jù)序列化協(xié)議:
- JSON: 是一種輕量級的數(shù)據(jù)交互格式,支持跨平臺、跨語言,被廣泛用在網(wǎng)絡(luò)間傳輸,JSON 的可讀性很強,但是序列化和反序列化性能卻是最差的,解析過程中,要產(chǎn)生大量的臨時變量,會頻繁的觸發(fā) GC,為了保證可讀性,并沒有進行二進制壓縮,當數(shù)據(jù)量很大的時候,性能上會差一點。
- ProtoBuffer:它是 Google 開源的跨語言編碼協(xié)議,可以應(yīng)用到 C++ 、C# 、Dart 、Go 、Java 、Python 等等語言,Google 內(nèi)部幾乎所有 RPC 都在使用這個協(xié)議,使用了二進制編碼壓縮,體積更小,速度比 JSON 更快,但是缺點是犧牲了可讀性
- FlatBuffers :同 Protocol Buffers 一樣是 Google 開源的跨平臺數(shù)據(jù)序列化庫,可以應(yīng)用到 C++ 、 C# , Go 、 Java 、 JavaScript 、 PHP 、 Python 等等語言,空間和時間復(fù)雜度上比其他的方式都要好,在使用過程中,不需要額外的內(nèi)存,幾乎接近原始數(shù)據(jù)在內(nèi)存中的大小,但是缺點是犧牲了可讀性,是為游戲或者其他對性能要求很高的應(yīng)用開發(fā)的。
使用
Preferences DataStore
基本使用流程
- 引入
def dataStoreVersion = '1.0.0-beta01' implementation "androidx.datastore:datastore-preferences:$dataStoreVersion"
- 創(chuàng)建DataStore
//指定DataStore的文件名 //對應(yīng)最終件:/data/data/org.geekbang.aac/files/datastore/user_preferences.preferences_pb private const val USER_PREFERENCES_NAME = "user_preferences" //擴展屬性DataStore,實際類型為DataStore<Preferences> private val Context.dataStore by preferencesDataStore( name = USER_PREFERENCES_NAME,//指定名稱 produceMigrations = {context -> //指定要恢復(fù)的sp文件,無需恢復(fù)可不寫 listOf(SharedPreferencesMigration(context, USER_PREFERENCES_NAME)) } )
- 定義Key
val SORT_ORDER = stringPreferencesKey("sort_order") val SHOW_COMPLETED = booleanPreferencesKey("show_completed") //... 通過查看源碼可以看到支持的其它數(shù)據(jù)類型
- 存儲
//edit要在suspend函數(shù)中 override suspend fun updateShowCompleted(showCompleted: Boolean) { dataStore.edit { preferences -> //...這里可以做一些數(shù)據(jù)的邏輯處理 preferences[SHOW_COMPLETED] = showCompleted // 整個tranform中的所有代碼塊被視為單個事務(wù) } }
- 讀取
override val userPreferencesFlow = dataStore.data .catch { exception -> if (exception is IOException) {//進行IO異常處理,確保能得到默認值 Log.e(TAG, "Error reading preferences.", exception) emit(emptyPreferences()) } else { throw exception } }.map { preferences -> //真正的獲取存儲的一個字段 val sortOrder = SortOrder.valueOf(preferences[SORT_ORDER] ?: SortOrder.NONE.name) val showCompleted = preferences[SHOW_COMPLETED] ?: false UserPreferences(showCompleted, sortOrder) }使用總結(jié)
一個對應(yīng)的preferences_pb文件對應(yīng)一個.kt文件,里面包含了文件名定義,DataStore定義,Key定義,存取方法定義;例如:
//TaskConfigDataStore.kt /** * 文件名 */ private const val TASK_CONFIG_PREFERENCES_FILE_NAME = "task_config_pre" /** * dataStore對象 */ val Context.taskConfigDataStore : DataStore<Preferences> by preferencesDataStore( name = TASK_CONFIG_PREFERENCES_FILE_NAME ) /** Keys **/ val SHOW_COMPLETED = booleanPreferencesKey("show_completed") val OPEN_COUNT = intPreferencesKey("open_count") //other keys /** 存取方法 **/ fun getShowCompleted(context: Context): Flow<Boolean> = context.taskConfigDataStore.data .catch { e-> if(e is IOException){ emptyPreferences() }else{ throw e } }.map { pre-> pre[SHOW_COMPLETED] ?: false } suspend fun setShowCompleted(context: Context,showComplete: Boolean){ context.taskConfigDataStore.edit { pre-> pre[SHOW_COMPLETED] = showComplete } } // other method
ProtoBuf DataStore
基本使用流程
- 接入protobuf,以最新的為準 詳情信息可參考protobuf-gradle-plugin,想詳細了解protobuf基礎(chǔ)知識,可參考Protobuf 終極教程
- 在xxx.build中加入:
plugins { //other... id "com.google.protobuf" version "0.8.16" }
- dependencies
// protobuf def protobufVersion = "3.10.0" // 3.0.0后Android建議使用javalite implementation "com.google.protobuf:protobuf-javalite:$protobufVersion"
- 增加protobuf 的塊
protobuf { protoc { artifact = "com.google.protobuf:protoc:3.10.0" } generateProtoTasks { all().each { task -> task.builtins { java { option 'lite' } } } } }
- 在src/main/目錄下建立proto文件,3.8.0以后自動識別此目錄下的.proto文件
- 引入dataStore庫
// dataStore def dataStoreVersion = '1.0.0-beta01' implementation "androidx.datastore:datastore:$dataStoreVersion"
- 建立proto文件后,進行rebuild
syntax = "proto3"; option java_package = "org.geekbang.aac"; option java_multiple_files = true; message UserPreferences { bool show_completed = 1; enum SortOrder { UNSPECIFIED = 0; NONE = 1; BY_DEADLINE = 2; BY_PRIORITY = 3; BY_DEADLINE_AND_PRIORITY = 4; } SortOrder sort_order = 2; }
- 創(chuàng)建Serializer的實現(xiàn),告訴框架如何讀寫,這個接口明確規(guī)定要有默認值,以便在尚未創(chuàng)建任何文件時使用,這是必要流程,基本是固定寫法,用編譯器生成的Java類對應(yīng)api即可
object UserPreferencesSerializer : Serializer<UserPreferences> { override val defaultValue: UserPreferences = UserPreferences.getDefaultInstance() @Suppress("BlockingMethodInNonBlockingContext") override suspend fun readFrom(input: InputStream): UserPreferences { try { return UserPreferences.parseFrom(input) } catch (exception: InvalidProtocolBufferException) { throw CorruptionException("Cannot read proto.", exception) } } @Suppress("BlockingMethodInNonBlockingContext") override suspend fun writeTo(t: UserPreferences, output: OutputStream) = t.writeTo(output) }
- 定義創(chuàng)建DataStore對象
//老的sp的文件名 private const val USER_PREFERENCES_NAME = "user_preferences" //新的文件名,對應(yīng)目錄 /data/data/com.codelab.android.datastore/files/datastore/user_prefs.pb private const val DATA_STORE_FILE_NAME = "user_prefs.pb" //老的對應(yīng)的key private const val SORT_ORDER_KEY = "sort_order" // Build the DataStore private val Context.userPreferencesStore: DataStore<UserPreferences> by dataStore( fileName = DATA_STORE_FILE_NAME, serializer = UserPreferencesSerializer, produceMigrations = { context -> listOf( SharedPreferencesMigration( context, USER_PREFERENCES_NAME ) { sharedPrefs: SharedPreferencesView, currentData: UserPreferences -> // 定義從SharedPreferences到UserPreference的映射 if (currentData.sortOrder == SortOrder.UNSPECIFIED) { currentData.toBuilder().setSortOrder( SortOrder.valueOf( sharedPrefs.getString(SORT_ORDER_KEY, SortOrder.NONE.name)!! ) ).build() } else { currentData } } ) } )
- 存儲
//必須是掛起函數(shù),決定其要在協(xié)程中使用 suspend fun updateShowCompleted(completed: Boolean) { //Proto DataStore 提供了一個updateData() 函數(shù), //用于以事務(wù)方式更新存儲的對象 //為您提供數(shù)據(jù)的當前狀態(tài),作為數(shù)據(jù)類型的一個實例,并在原子讀-寫-修改操作中以事務(wù)方式更新數(shù)據(jù) userPreferencesStore.updateData { currentPreferences ->//當前文件對應(yīng)的對象 currentPreferences.toBuilder().setShowCompleted(completed).build()//對當前對象進行修改 } }
- 讀取
val userPreferencesFlow: Flow<UserPreferences> = userPreferencesStore.data .catch { exception -> // dataStore.data throws an IOException when an error is encountered when reading > data if (exception is IOException) { Log.e(TAG, "Error reading sort order preferences.", exception) emit(UserPreferences.getDefaultInstance()) } else { throw exception } } //單獨獲取時是阻塞的,在實際使用中建議是異步的,在Kotlin項目中可以使用協(xié)程異步實現(xiàn) suspend fun getUserPreferencesFlowData() = userPreferencesFlow.first()使用總結(jié)
這個是面向相對復(fù)雜的對象結(jié)構(gòu)(例如用戶信息的本地緩存)的場景下使用,一般以一個proto文件為單位,相關(guān)定義,方法做好整體分類即可。
幾點
- DataStore獲取返回的是流,流進行collect時,統(tǒng)一協(xié)程內(nèi)只有第一次collect會收到流更新
- 一個DataStore有多個key時,任意一個更新時,都會觸發(fā)流的collect,這點決定DataStore在使用時不易向sp那樣綜合使用,可能會引發(fā)沒必要的回調(diào)
- 上面的更新,必須是改變,重復(fù)設(shè)置相同的值不算更新。
- 一定得同步獲取值時,可以用runBlocking進行阻塞獲取,不過這個并不是官方本意,實在需要可以結(jié)合dataStore.data.first()方法進行預(yù)加載,這個可以將最新值緩存到內(nèi)存,再同步獲取時能更高效,這里比如我們的一些Header相關(guān)的,可以在啟動app時異步預(yù)加載一下。