概念
- 轻量级数据存储方案
- 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
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
基本使用流程
- 引入
def dataStoreVersion = '1.0.0-beta01' implementation "androidx.datastore:datastore-preferences:$dataStoreVersion"
- 创建DataStore
//指定DataStore的文件名 //对应最终件:/data/data/org.geekbang.aac/files/datastore/user_preferences.preferences_pb private const val USER_PREFERENCES_NAME = "user_preferences" //扩展属性DataStore,实际类型为DataStore
private val Context.dataStore by preferencesDataStore( name = USER_PREFERENCES_NAME,//指定名称 produceMigrations = {context -> //指定要恢复的sp文件,无需恢复可不写 listOf(SharedPreferencesMigration(context, USER_PREFERENCES_NAME)) } )
- 定义Key
val SORT_ORDER = stringPreferencesKey("sort_order") val SHOW_COMPLETED = booleanPreferencesKey("show_completed") //... 通过查看源码可以看到支持的其它数据类型
- 存储
//edit要在suspend函数中 override suspend fun updateShowCompleted(showCompleted: Boolean) { dataStore.edit { preferences -> //...这里可以做一些数据的逻辑处理 preferences[SHOW_COMPLETED] = showCompleted // 整个tranform中的所有代码块被视为单个事务 } }
- 读取
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
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 = 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基础知识,可参考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; }
- 创建Serializer的实现,告诉框架如何读写,这个接口明确规定要有默认值,以便在尚未创建任何文件时使用,这是必要流程,基本是固定写法,用编译器生成的Java类对应api即可
object UserPreferencesSerializer : Serializer
{ 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) }
- 定义创建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
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 } } ) } )
- 存储
//必须是挂起函数,决定其要在协程中使用 suspend fun updateShowCompleted(completed: Boolean) { //Proto DataStore 提供了一个updateData() 函数, //用于以事务方式更新存储的对象 //为您提供数据的当前状态,作为数据类型的一个实例,并在原子读-写-修改操作中以事务方式更新数据 userPreferencesStore.updateData { currentPreferences ->//当前文件对应的对象 currentPreferences.toBuilder().setShowCompleted(completed).build()//对当前对象进行修改 } }
- 读取
val userPreferencesFlow: Flow
= 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文件为单位,相关定义,方法做好整体分类即可。
几点
- DataStore获取返回的是流,流进行collect时,统一协程内只有第一次collect会收到流更新
- 一个DataStore有多个key时,任意一个更新时,都会触发流的collect,这点决定DataStore在使用时不易向sp那样综合使用,可能会引发没必要的回调
- 上面的更新,必须是改变,重复设置相同的值不算更新。
- 一定得同步获取值时,可以用runBlocking进行阻塞获取,不过这个并不是官方本意,实在需要可以结合dataStore.data.first()方法进行预加载,这个可以将最新值缓存到内存,再同步获取时能更高效,这里比如我们的一些Header相关的,可以在启动app时异步预加载一下。