DataStore概念與實(shí)踐

概念

  • 輕量級(jí)數(shù)據(jù)存儲(chǔ)方案
  • Kotlin Countinue+Flow 以異步圈膏,一致的事務(wù)方式存儲(chǔ)數(shù)據(jù)
  • SharedPrefderences方案的替代者
    • Sp的痛點(diǎn)
      詳情參見再見 SharedPreferences 擁抱 Jetpack DataStore
      • getXXX可能會(huì)阻塞主線程:在同步方法內(nèi)調(diào)用了 wait() 方法,會(huì)一直等待 getSharedPreferences() 方法開啟的線程讀取完數(shù)據(jù)才能繼續(xù)往下執(zhí)行
      • 類型不一定安全:相同的key,putInt(key,0)处嫌,getString(key)脏嚷,就會(huì)出現(xiàn)ClassCastException異常
      • Sp加載的數(shù)據(jù)會(huì)一直存在內(nèi)存中:通過(guò)靜態(tài)的 ArrayMap 緩存每一個(gè) SP 文件各淀,而每個(gè) SP 文件內(nèi)容通過(guò) Map 緩存鍵值對(duì)數(shù)據(jù)妇穴,這樣數(shù)據(jù)會(huì)一直留在內(nèi)存中灸姊,浪費(fèi)內(nèi)存拱燃。
      • apply方法是異步的,但可能會(huì)導(dǎo)致ANR:當(dāng)生命周期處于 handleStopService() 力惯、 handlePauseActivity() 碗誉、 handleStopActivity() 的時(shí)候會(huì)一直等待 apply() 方法將數(shù)據(jù)保存成功召嘶,否則會(huì)一直等待,從而阻塞主線程造成 ANR
      • SP 不能用于跨進(jìn)程通信:當(dāng)遇到 MODE_MULTI_PROCESS 的時(shí)候哮缺,會(huì)重新讀取 SP 文件內(nèi)容苍蔬,并不能用 SP 來(lái)做跨進(jìn)程通信。
    • DataStore的優(yōu)勢(shì)
      • DataStore 是基于 Flow 實(shí)現(xiàn)的蝴蜓,所以保證了在主線程的安全性
      • 以事務(wù)方式處理更新數(shù)據(jù)碟绑,事務(wù)有四大特性(原子性、一致性茎匠、 隔離性格仲、持久性)
      • 沒有 apply() 和 commit() 等等數(shù)據(jù)持久的方法
      • 自動(dòng)完成 SharedPreferences 遷移到 DataStore,保證數(shù)據(jù)一致性诵冒,不會(huì)造成數(shù)據(jù)損壞
      • 可以監(jiān)聽到操作成功或者失敗結(jié)果
  • Preferences DataStore (鍵值對(duì)) 方式
    • Preference DataStore 本質(zhì)也是proto buffer存儲(chǔ)凯肋,只是這個(gè)proto文件時(shí)框架自己提供的,對(duì)應(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文件可完全自定義侮东,類型更加靈活
    • 序列化:對(duì)象->可存儲(chǔ)傳輸?shù)淖止?jié)序列;反序列化倒過(guò)來(lái)
    • 數(shù)據(jù)序列化協(xié)議:
      • JSON: 是一種輕量級(jí)的數(shù)據(jù)交互格式豹芯,支持跨平臺(tái)悄雅、跨語(yǔ)言,被廣泛用在網(wǎng)絡(luò)間傳輸铁蹈,JSON 的可讀性很強(qiáng)宽闲,但是序列化和反序列化性能卻是最差的,解析過(guò)程中握牧,要產(chǎn)生大量的臨時(shí)變量容诬,會(huì)頻繁的觸發(fā) GC,為了保證可讀性沿腰,并沒有進(jìn)行二進(jìn)制壓縮览徒,當(dāng)數(shù)據(jù)量很大的時(shí)候,性能上會(huì)差一點(diǎn)颂龙。
      • ProtoBuffer:它是 Google 開源的跨語(yǔ)言編碼協(xié)議习蓬,可以應(yīng)用到 C++ 、C# 厘托、Dart 友雳、Go 稿湿、Java 铅匹、Python 等等語(yǔ)言,Google 內(nèi)部幾乎所有 RPC 都在使用這個(gè)協(xié)議饺藤,使用了二進(jìn)制編碼壓縮包斑,體積更小流礁,速度比 JSON 更快,但是缺點(diǎn)是犧牲了可讀性
      • FlatBuffers :同 Protocol Buffers 一樣是 Google 開源的跨平臺(tái)數(shù)據(jù)序列化庫(kù)罗丰,可以應(yīng)用到 C++ 神帅、 C# , Go 、 Java 萌抵、 JavaScript 找御、 PHP 、 Python 等等語(yǔ)言绍填,空間和時(shí)間復(fù)雜度上比其他的方式都要好霎桅,在使用過(guò)程中,不需要額外的內(nèi)存讨永,幾乎接近原始數(shù)據(jù)在內(nèi)存中的大小滔驶,但是缺點(diǎn)是犧牲了可讀性,是為游戲或者其他對(duì)性能要求很高的應(yīng)用開發(fā)的卿闹。

使用

Preferences DataStore
基本使用流程
  1. 引入
def dataStoreVersion = '1.0.0-beta01'
implementation "androidx.datastore:datastore-preferences:$dataStoreVersion"
  1. 創(chuàng)建DataStore
//指定DataStore的文件名
//對(duì)應(yīng)最終件:/data/data/org.geekbang.aac/files/datastore/user_preferences.preferences_pb
private const val USER_PREFERENCES_NAME = "user_preferences"
//擴(kuò)展屬性DataStore揭糕,實(shí)際類型為DataStore<Preferences>
private val Context.dataStore by preferencesDataStore(
    name = USER_PREFERENCES_NAME,//指定名稱
    produceMigrations = {context ->  //指定要恢復(fù)的sp文件,無(wú)需恢復(fù)可不寫
        listOf(SharedPreferencesMigration(context, USER_PREFERENCES_NAME))
    }
)
  1. 定義Key
val SORT_ORDER = stringPreferencesKey("sort_order")
val SHOW_COMPLETED = booleanPreferencesKey("show_completed")
//... 通過(guò)查看源碼可以看到支持的其它數(shù)據(jù)類型
  1. 存儲(chǔ)
//edit要在suspend函數(shù)中
override suspend fun updateShowCompleted(showCompleted: Boolean) {
        dataStore.edit { preferences ->
            //...這里可以做一些數(shù)據(jù)的邏輯處理
           preferences[SHOW_COMPLETED] = showCompleted
            // 整個(gè)tranform中的所有代碼塊被視為單個(gè)事務(wù)
        }
    }
  1. 讀取
override val userPreferencesFlow = dataStore.data
        .catch { exception ->
            if (exception is IOException) {//進(jìn)行IO異常處理锻霎,確保能得到默認(rèn)值
                Log.e(TAG, "Error reading preferences.", exception)
                emit(emptyPreferences())
            } else {
                throw exception
            }
        }.map { preferences ->
            //真正的獲取存儲(chǔ)的一個(gè)字段
            val sortOrder = SortOrder.valueOf(preferences[SORT_ORDER] ?: SortOrder.NONE.name)
            val showCompleted = preferences[SHOW_COMPLETED] ?: false
            UserPreferences(showCompleted, sortOrder)
        }
使用總結(jié)

一個(gè)對(duì)應(yīng)的preferences_pb文件對(duì)應(yīng)一個(gè).kt文件著角,里面包含了文件名定義,DataStore定義旋恼,Key定義雇寇,存取方法定義;例如:

//TaskConfigDataStore.kt
/**
 * 文件名
 */
private const val TASK_CONFIG_PREFERENCES_FILE_NAME = "task_config_pre"

/**
 * dataStore對(duì)象
 */
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
基本使用流程
  1. 接入protobuf蚌铜,以最新的為準(zhǔn) 詳情信息可參考protobuf-gradle-plugin锨侯,想詳細(xì)了解protobuf基礎(chǔ)知識(shí),可參考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以后自動(dòng)識(shí)別此目錄下的.proto文件
  2. 引入dataStore庫(kù)
// dataStore
 def dataStoreVersion = '1.0.0-beta01'
 implementation  "androidx.datastore:datastore:$dataStoreVersion"
  1. 建立proto文件后囚痴,進(jìn)行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;
}
  1. 創(chuàng)建Serializer的實(shí)現(xiàn),告訴框架如何讀寫审葬,這個(gè)接口明確規(guī)定要有默認(rèn)值深滚,以便在尚未創(chuàng)建任何文件時(shí)使用,這是必要流程,基本是固定寫法涣觉,用編譯器生成的Java類對(duì)應(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)
}
  1. 定義創(chuàng)建DataStore對(duì)象
//老的sp的文件名
private const val USER_PREFERENCES_NAME = "user_preferences"
//新的文件名痴荐,對(duì)應(yīng)目錄 /data/data/com.codelab.android.datastore/files/datastore/user_prefs.pb
private const val DATA_STORE_FILE_NAME = "user_prefs.pb"
//老的對(duì)應(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
                }
            }
        )
    }
)
  1. 存儲(chǔ)
//必須是掛起函數(shù),決定其要在協(xié)程中使用
suspend fun updateShowCompleted(completed: Boolean) {
//Proto DataStore 提供了一個(gè)updateData() 函數(shù)官册,
//用于以事務(wù)方式更新存儲(chǔ)的對(duì)象
//為您提供數(shù)據(jù)的當(dāng)前狀態(tài)生兆,作為數(shù)據(jù)類型的一個(gè)實(shí)例,并在原子讀-寫-修改操作中以事務(wù)方式更新數(shù)據(jù)
        userPreferencesStore.updateData { currentPreferences ->//當(dāng)前文件對(duì)應(yīng)的對(duì)象
            currentPreferences.toBuilder().setShowCompleted(completed).build()//對(duì)當(dāng)前對(duì)象進(jìn)行修改
        }
    }
  1. 讀取
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
            }
        }
//單獨(dú)獲取時(shí)是阻塞的膝宁,在實(shí)際使用中建議是異步的鸦难,在Kotlin項(xiàng)目中可以使用協(xié)程異步實(shí)現(xiàn)
suspend fun getUserPreferencesFlowData() = userPreferencesFlow.first()
使用總結(jié)

這個(gè)是面向相對(duì)復(fù)雜的對(duì)象結(jié)構(gòu)(例如用戶信息的本地緩存)的場(chǎng)景下使用根吁,一般以一個(gè)proto文件為單位,相關(guān)定義合蔽,方法做好整體分類即可击敌。

幾點(diǎn)

  1. DataStore獲取返回的是流,流進(jìn)行collect時(shí)拴事,統(tǒng)一協(xié)程內(nèi)只有第一次collect會(huì)收到流更新
  2. 一個(gè)DataStore有多個(gè)key時(shí)沃斤,任意一個(gè)更新時(shí),都會(huì)觸發(fā)流的collect刃宵,這點(diǎn)決定DataStore在使用時(shí)不易向sp那樣綜合使用轰枝,可能會(huì)引發(fā)沒必要的回調(diào)
  3. 上面的更新,必須是改變组去,重復(fù)設(shè)置相同的值不算更新鞍陨。
  4. 一定得同步獲取值時(shí),可以用runBlocking進(jìn)行阻塞獲取从隆,不過(guò)這個(gè)并不是官方本意诚撵,實(shí)在需要可以結(jié)合dataStore.data.first()方法進(jìn)行預(yù)加載,這個(gè)可以將最新值緩存到內(nèi)存键闺,再同步獲取時(shí)能更高效寿烟,這里比如我們的一些Header相關(guān)的,可以在啟動(dòng)app時(shí)異步預(yù)加載一下辛燥。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末筛武,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子挎塌,更是在濱河造成了極大的恐慌徘六,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,723評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件榴都,死亡現(xiàn)場(chǎng)離奇詭異待锈,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)嘴高,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門竿音,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人拴驮,你說(shuō)我怎么就攤上這事春瞬。” “怎么了套啤?”我有些...
    開封第一講書人閱讀 152,998評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵宽气,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng)抹竹,這世上最難降的妖魔是什么线罕? 我笑而不...
    開封第一講書人閱讀 55,323評(píng)論 1 279
  • 正文 為了忘掉前任止潮,我火速辦了婚禮窃判,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘喇闸。我一直安慰自己袄琳,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評(píng)論 5 374
  • 文/花漫 我一把揭開白布燃乍。 她就那樣靜靜地躺著唆樊,像睡著了一般。 火紅的嫁衣襯著肌膚如雪刻蟹。 梳的紋絲不亂的頭發(fā)上逗旁,一...
    開封第一講書人閱讀 49,079評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音舆瘪,去河邊找鬼片效。 笑死,一個(gè)胖子當(dāng)著我的面吹牛英古,可吹牛的內(nèi)容都是我干的淀衣。 我是一名探鬼主播,決...
    沈念sama閱讀 38,389評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼召调,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼膨桥!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起唠叛,我...
    開封第一講書人閱讀 37,019評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤只嚣,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后艺沼,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體介牙,經(jīng)...
    沈念sama閱讀 43,519評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評(píng)論 2 325
  • 正文 我和宋清朗相戀三年澳厢,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了环础。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,100評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡剩拢,死狀恐怖线得,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情徐伐,我是刑警寧澤贯钩,帶...
    沈念sama閱讀 33,738評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響角雷,放射性物質(zhì)發(fā)生泄漏祸穷。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評(píng)論 3 307
  • 文/蒙蒙 一勺三、第九天 我趴在偏房一處隱蔽的房頂上張望雷滚。 院中可真熱鬧,春花似錦吗坚、人聲如沸祈远。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)车份。三九已至,卻和暖如春牡彻,著一層夾襖步出監(jiān)牢的瞬間扫沼,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工庄吼, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留缎除,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,547評(píng)論 2 354
  • 正文 我出身青樓霸褒,卻偏偏與公主長(zhǎng)得像伴找,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子废菱,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • 作者 / Android 開發(fā)技術(shù)推廣工程師 Florina Muntenescu 與 Google 軟件工程師 ...
    谷歌開發(fā)者閱讀 971評(píng)論 0 4
  • Jetpack 的 DataStore 是一種數(shù)據(jù)存儲(chǔ)解決方案技矮,可以像 SharedPreferences 一樣存...
    TTTqiu閱讀 1,676評(píng)論 1 2
  • 表情是什么,我認(rèn)為表情就是表現(xiàn)出來(lái)的情緒殊轴。表情可以傳達(dá)很多信息衰倦。高興了當(dāng)然就笑了,難過(guò)就哭了旁理。兩者是相互影響密不可...
    Persistenc_6aea閱讀 124,193評(píng)論 2 7
  • 16宿命:用概率思維提高你的勝算 以前的我是風(fēng)險(xiǎn)厭惡者樊零,不喜歡去冒險(xiǎn),但是人生放棄了冒險(xiǎn)孽文,也就放棄了無(wú)數(shù)的可能驻襟。 ...
    yichen大刀閱讀 6,033評(píng)論 0 4