Android Jetpack组件系列文章:
Android Jetpack组件(一)LifeCycle
Android Jetpack组件(二)Navigation
Android Jetpack组件(三)ViewModel
Android Jetpack组件(四)LiveData
Android Jetpack组件(五)Room
Android JetPack组件(六)DataBinding
Android Jetpack组件(七)Paging
Android Jetpack组件(八)WorkManager
Android Jetpack组件(九)DataStore
Android Jetpack组件(十)App Startup
Android Jetpack组件(十一)Compose
疫情距离我最近的一次,隔离的第10天,居家办公的第8天,希望疫情早点过去,结束隔离✊。
首语
数据持久化指将哪些内存中的瞬时数据保存到存储设备中,保证即使在手机或电脑关机的情况下,数据依然不会丢失。
Android系统中主要提供了三种方式来实现数据持久化功能。即文件存储、SharedPreferences存储及数据库存储。其中SharedPreferences是使用键值对的方式来存储轻量型数据,使用比较简单,且程序卸载后也会一并清除,不会残留数据。但是SharedPreferences也存在很多缺点,它是对磁盘进行I/O操作,会引起性能问题,导致ANR,且多线程场景下效率低下、存储延迟,存储较大数据如json或html会频繁引起GC,导致界面卡顿,曾经在项目开发中使用SharedPreferences碰到数据缓存延迟的情况,后面就使用了腾讯的MMKV。
现在,Google推出DataStore,旨在代替SharedPreferences,克服其大部分缺点。
Jetpack DataStore 是一种数据存储解决方案,允许您使用协议缓冲区存储键值对或类型化对象。DataStore 使用 Kotlin 协程和 Flow 以异步、一致的事务方式存储数据。
对比
DataStore 提供两种不同的实现:Preferences DataStore 和 Proto DataStore。
- Preferences DataStore 由类 DataStore 和 Preferences 实现,使用键存储和访问数据。此实现不需要预定义的架构,也不确保类型安全。
- Proto DataStore 将数据作为自定义数据类型的实例序列化存储在磁盘。此实现要求您使用协议缓冲区(Protocol Buffers)来定义架构,但可以确保类型安全。
Protocol Buffers (ProtocolBuffer/ protobuf )是Google公司开发的一种数据描述语言,类似于XML能够将结构化数据序列化,可用于数据存储、通信协议等方面。同XML相比,Protocol buffers在序列化结构化数据方面有许多优点:
-
更简单。
-
数据描述文件只需原来的1/10至1/3。
-
解析速度是原来的20倍至100倍。
-
减少了二义性。
-
生成了更容易在编程中使用的数据访问类。
下图是Google对SharedPreferences与DataStore两种不同实现的对比。
依赖
// Preferences DataStore
implementation "androidx.datastore:datastore-preferences:1.0.0"
// Proto DataStore
implementation "androidx.datastore:datastore-core:1.0.0"
使用
在两种实现中,除非另外特指,否则 DataStore 会将首选项存储在文件中,并且所有的数据操作都会在 Dispatchers.IO 上执行。
Preferences DataStore
创建
使用由PreferencesDataStore
创建的属性委托来创建 Datastore<Preferences>
实例。在 kotlin 文件顶层调用该实例一次,便可在应用的所有其余部分通过此属性访问该实例。这样可以更轻松地将 DataStore
保留为单例。
val dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
读取
由于 Preferences DataStore 不使用预定义的架构,因此您必须使用相应的键类型函数为需要存储在 DataStore<Preferences>
实例中的每个值定义一个键。例如,如需为 int 值定义一个键,请使用 intPreferencesKey()
。。然后,使用 DataStore.data
属性读取内容。
val USER_INFO = intPreferencesKey("user_info")
dataStore.data.collect {
val i = it.toPreferences()[USER_INFO]
tvContent.text=i.toString()
}
写入
Preferences DataStore 提供了一个edit()
函数,用于以事务方式更新 DataStore
中的数据。该函数的 transform
参数接受代码块,您可以在其中根据需要更新值。转换块中的所有代码均被视为单个事务,查看源码可知。
dataStore.edit { settings ->
val currentCounterValue = settings[USER_INFO] ?: 0
settings[USER_INFO] = currentCounterValue + 1
}
Proto DataStore
Proto DataStore 实现使用 DataStore 和协议缓冲区将类型化的对象保留在磁盘上。
导入
- 导入plugins 插件。在app的build.gradle中添加如下代码。
plugins {
id "com.android.application"
id "kotlin-android"
id "com.google.protobuf" version "0.8.12"
}
- 添加依赖。
implementation "com.google.protobuf:protobuf-javalite:3.10.0"
- 配置 protoc 命令,与dependencies同级。
protobuf {
protoc {
// //从仓库下载 protoc 这里的版本号需要与依赖 com.google.protobuf:protobuf-javalite:xxx 版本相同
artifact = 'com.google.protobuf:protoc:3.10.0'
}
generateProtoTasks {
all().each { task ->
task.builtins {
java {
option "lite"
}
}
}
}
// 默认生成目录 $buildDir/generated/source/proto 通过 generatedFilesBaseDir 改变生成位置
generatedFilesBaseDir = "$projectDir/src/main"
}
- 设置proto文件位置。
android {
sourceSets {
main {
proto {
// proto 文件默认路径是 src/main/proto
// 可以通过 srcDir 修改 proto 文件的位置
srcDir 'src/main/proto'
}
}
}
}
- 编译项目。
在app/src/main目录下新建一个文件夹proto,然后在文件夹proto下新建一个.proto类型的文件UserPrefs,编写proto文件及其字段,重新构建项目。
// 固定的,还有proto2
syntax="proto3";
// 格式:包名 + . + 文件名
option java_package = "com.yhj.kotlincomponent.protobuf";
//可以生成单独的.java每个生成的类的文件
option java_multiple_files = true;
message Settings {
int32 count = 1;
}
这里推荐安装 Protocol Buffer Editor插件,它为协议缓冲区文件提供编辑器支持。语法高亮、编辑器增强功能等有点,调试起来非常方便。
对于proto3语法,使用技巧,参考Google proto3教程,讲解详细。
构建完成后,就可以看到app\src\main\debug目录下生成了protobuf文件目录,里面包含Settings、SettingsOrBuilder、UserPrefs文件。
创建
定义一个实现 Serializer<T>
的类,其中 T
是 proto 文件中定义的类型。此序列化器类会告知 DataStore 如何读取和写入您的数据类型。请务必为该序列化器添加默认值,以便在尚未创建任何文件时使用。
使用由 dataStore
创建的属性委托来创建 DataStore<T>
的实例,其中 T
是在 proto 文件中定义的类型。在您的 Kotlin 文件顶层调用该实例一次,便可在应用的所有其余部分通过此属性委托访问该实例。filename
参数会告知 DataStore 使用哪个文件存储数据,而 serializer
参数会告知 DataStore 上面中定义的序列化器类的名称。
object SettingsSerializer : Serializer<Settings> {
override val defaultValue: Settings = Settings.getDefaultInstance()
override suspend fun readFrom(input: InputStream): Settings {
try {
return Settings.parseFrom(input)
} catch (exception: InvalidProtocolBufferException) {
throw CorruptionException("Cannot read proto.", exception)
}
}
override suspend fun writeTo(
t: Settings,
output: OutputStream
) = t.writeTo(output)
}
val Context.settingsDataStore: DataStore<Settings> by dataStore(
fileName = "settings.pb",
serializer = SettingsSerializer
)
写入
Proto DataStore 提供了一个updateData()
函数,用于以事务方式更新存储的对象。updateData()
为您提供数据的当前状态,作为数据类型的一个实例,并在原子读-写-修改操作中以事务方式更新数据。
settingsDataStore.updateData { currentSettings ->
currentSettings.toBuilder()
.setCount(currentSettings.count + 1)
.build()
}
读取
使用 DataStore.data
来获取存储的数据。
settingsDataStore.data.collect {
Log.e("yhj", it.count.toString(), )
}
迁移SharedPreferences
在创建DataStore时,preferencesDataStore参数里包含produceMigrations参数,用来迁移SharedPreferences,需要执行一次读取或者写入操作,DataStore才会自动合并,迁移成功后会删除原有的SharedPreferences文件,Proto DataStore 用法相同。
val dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings", produceMigrations = { con ->
listOf(SharedPreferencesMigration(con, "app_cache"))
})
原理
使用由PreferencesDataStore
创建的属性委托来创建 Datastore<Preferences>
实例。
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!!
}
}
创建了一个文件用于将键值对写入磁盘,文件位于applicationContext.filesDir+datastore/的子目录中。文件后缀名为.preferences_pb,
public fun Context.preferencesDataStoreFile(name: String): File =
this.dataStoreFile("$name.preferences_pb")
public fun Context.dataStoreFile(fileName: String): File =
File(applicationContext.filesDir, "datastore/$fileName")
数据的读取直接通过dataStore.data获取,数据的写入通过dataStore.edit,实际上也是通过dataStore.updateData来写入的
public interface DataStore<T> {
public val data: Flow<T>
public suspend fun updateData(transform: suspend (t: T) -> T): T
}
public suspend fun DataStore<Preferences>.edit(
transform: suspend (MutablePreferences) -> Unit
): Preferences {
return this.updateData {
// It's safe to return MutablePreferences since we freeze it in
// PreferencesDataStore.updateData()
it.toMutablePreferences().apply { transform(this) }
}
}
总结
DataStore的两种实现相比而言,Preferences DataStore相对简单一些,Proto DataStore比较复杂些。
DataStore克服了SharedPreference的许多缺点,Google也大力推荐,所以是时候跟SharedPreference说再见了,拥抱 Jetpack DataStore。