DataStore概念与实践

概念

  • 轻量级数据存储方案
  • Kotlin Countinue+Flow 以异步,一致的事务方式存储数据
  • SharedPrefderences方案的替代者
    • Sp的痛点
      详情参见再见 SharedPreferences 拥抱 Jetpack DataStore
      • getXXX可能会阻塞主线程:在同步方法内调用了 wait() 方法,会一直等待 getSharedPreferences() 方法开启的线程读取完数据才能继续往下执行
      • 类型不一定安全:相同的key,putInt(key,0),getString(key),就会出现ClassCastException异常
      • Sp加载的数据会一直存在内存中:通过静态的 ArrayMap 缓存每一个 SP 文件,而每个 SP 文件内容通过 Map 缓存键值对数据,这样数据会一直留在内存中,浪费内存。
      • apply方法是异步的,但可能会导致ANR:当生命周期处于 handleStopService() 、 handlePauseActivity() 、 handleStopActivity() 的时候会一直等待 apply() 方法将数据保存成功,否则会一直等待,从而阻塞主线程造成 ANR
      • SP 不能用于跨进程通信:当遇到 MODE_MULTI_PROCESS 的时候,会重新读取 SP 文件内容,并不能用 SP 来做跨进程通信。
    • DataStore的优势
      • DataStore 是基于 Flow 实现的,所以保证了在主线程的安全性
      • 以事务方式处理更新数据,事务有四大特性(原子性、一致性、 隔离性、持久性)
      • 没有 apply() 和 commit() 等等数据持久的方法
      • 自动完成 SharedPreferences 迁移到 DataStore,保证数据一致性,不会造成数据损坏
      • 可以监听到操作成功或者失败结果
  • Preferences DataStore (键值对) 方式
    • Preference DataStore 本质也是proto buffer存储,只是这个proto文件时框架自己提供的,对应的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文件可完全自定义,类型更加灵活
    • 序列化:对象->可存储传输的字节序列;反序列化倒过来
    • 数据序列化协议:
      • JSON: 是一种轻量级的数据交互格式,支持跨平台、跨语言,被广泛用在网络间传输,JSON 的可读性很强,但是序列化和反序列化性能却是最差的,解析过程中,要产生大量的临时变量,会频繁的触发 GC,为了保证可读性,并没有进行二进制压缩,当数据量很大的时候,性能上会差一点。
      • ProtoBuffer:它是 Google 开源的跨语言编码协议,可以应用到 C++ 、C# 、Dart 、Go 、Java 、Python 等等语言,Google 内部几乎所有 RPC 都在使用这个协议,使用了二进制编码压缩,体积更小,速度比 JSON 更快,但是缺点是牺牲了可读性
      • FlatBuffers :同 Protocol Buffers 一样是 Google 开源的跨平台数据序列化库,可以应用到 C++ 、 C# , Go 、 Java 、 JavaScript 、 PHP 、 Python 等等语言,空间和时间复杂度上比其他的方式都要好,在使用过程中,不需要额外的内存,几乎接近原始数据在内存中的大小,但是缺点是牺牲了可读性,是为游戏或者其他对性能要求很高的应用开发的。

使用

Preferences DataStore
基本使用流程
  1. 引入
def dataStoreVersion = '1.0.0-beta01'
implementation "androidx.datastore:datastore-preferences:$dataStoreVersion"
  1. 创建DataStore
//指定DataStore的文件名
//对应最终件:/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 ->  //指定要恢复的sp文件,无需恢复可不写
        listOf(SharedPreferencesMigration(context, USER_PREFERENCES_NAME))
    }
)
  1. 定义Key
val SORT_ORDER = stringPreferencesKey("sort_order")
val SHOW_COMPLETED = booleanPreferencesKey("show_completed")
//... 通过查看源码可以看到支持的其它数据类型
  1. 存储
//edit要在suspend函数中
override suspend fun updateShowCompleted(showCompleted: Boolean) {
        dataStore.edit { preferences ->
            //...这里可以做一些数据的逻辑处理
           preferences[SHOW_COMPLETED] = showCompleted
            // 整个tranform中的所有代码块被视为单个事务
        }
    }
  1. 读取
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)
        }
使用总结

一个对应的preferences_pb文件对应一个.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
基本使用流程
  1. 接入protobuf,以最新的为准 详情信息可参考protobuf-gradle-plugin,想详细了解protobuf基础知识,可参考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文件
  2. 引入dataStore库
// dataStore
 def dataStoreVersion = '1.0.0-beta01'
 implementation  "androidx.datastore:datastore:$dataStoreVersion"
  1. 建立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;
}
  1. 创建Serializer的实现,告诉框架如何读写,这个接口明确规定要有默认值,以便在尚未创建任何文件时使用,这是必要流程,基本是固定写法,用编译器生成的Java类对应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. 定义创建DataStore对象
//老的sp的文件名
private const val USER_PREFERENCES_NAME = "user_preferences"
//新的文件名,对应目录 /data/data/com.codelab.android.datastore/files/datastore/user_prefs.pb
private const val DATA_STORE_FILE_NAME = "user_prefs.pb"
//老的对应的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. 存储
//必须是挂起函数,决定其要在协程中使用
suspend fun updateShowCompleted(completed: Boolean) {
//Proto DataStore 提供了一个updateData() 函数,
//用于以事务方式更新存储的对象
//为您提供数据的当前状态,作为数据类型的一个实例,并在原子读-写-修改操作中以事务方式更新数据
        userPreferencesStore.updateData { currentPreferences ->//当前文件对应的对象
            currentPreferences.toBuilder().setShowCompleted(completed).build()//对当前对象进行修改
        }
    }
  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
            }
        }
//单独获取时是阻塞的,在实际使用中建议是异步的,在Kotlin项目中可以使用协程异步实现
suspend fun getUserPreferencesFlowData() = userPreferencesFlow.first()
使用总结

这个是面向相对复杂的对象结构(例如用户信息的本地缓存)的场景下使用,一般以一个proto文件为单位,相关定义,方法做好整体分类即可。

几点

  1. DataStore获取返回的是流,流进行collect时,统一协程内只有第一次collect会收到流更新
  2. 一个DataStore有多个key时,任意一个更新时,都会触发流的collect,这点决定DataStore在使用时不易向sp那样综合使用,可能会引发没必要的回调
  3. 上面的更新,必须是改变,重复设置相同的值不算更新。
  4. 一定得同步获取值时,可以用runBlocking进行阻塞获取,不过这个并不是官方本意,实在需要可以结合dataStore.data.first()方法进行预加载,这个可以将最新值缓存到内存,再同步获取时能更高效,这里比如我们的一些Header相关的,可以在启动app时异步预加载一下。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,519评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,842评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,544评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,742评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,646评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,027评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,513评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,169评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,324评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,268评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,299评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,996评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,591评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,667评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,911评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,288评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,871评论 2 341

推荐阅读更多精彩内容

  • 作者 / Android 开发技术推广工程师 Florina Muntenescu 与 Google 软件工程师 ...
    谷歌开发者阅读 966评论 0 4
  • Jetpack 的 DataStore 是一种数据存储解决方案,可以像 SharedPreferences 一样存...
    TTTqiu阅读 1,669评论 1 2
  • 表情是什么,我认为表情就是表现出来的情绪。表情可以传达很多信息。高兴了当然就笑了,难过就哭了。两者是相互影响密不可...
    Persistenc_6aea阅读 123,641评论 2 7
  • 16宿命:用概率思维提高你的胜算 以前的我是风险厌恶者,不喜欢去冒险,但是人生放弃了冒险,也就放弃了无数的可能。 ...
    yichen大刀阅读 6,025评论 0 4