DataStore 是一个 Jetpack 数据存储框架,为少量、简单的数据存储,提供了一种安全、一致的方式。DataStore 的推出主要目的是为了取代 SharedPreferences。DataStore 提供两种不同的实现:Preferences DataStore 和 Proto DataStore,其中 Preferences DataStore 是以 key-value 形式存储数据的;而 Proto DataStore 是以 Proto 形式存储数据的。
DataStore 的特点
SharedPreferences 和 DataStore 之间的对比图如下所示:
在异步接口和同步工作上:DataStore本身是基于 Kotlin 协程和 Flow 实现异步数据存储的,因此它不支持做同步工作。相反
SharedPreferences 有 commit 这个接口来同步提交数据。但是它是有风险的,可能会导致 ANR 和卡顿。
在异常处理上:当数据解析错误时,SharedPreferences 会抛出运行时异常,例如 ClassCastException 异常。而 DataStore 则可以依靠 Flow 的异常处理机制,捕获读写数据时出现的任何异常。
在类型安全上:Preferences DataStore 和 SharedPreferences 都不能提供类型安全保护。而使用 Proto DataStore,则可以为数据预定义结构,从而确保类型安全。
在数据一致性上:SharedPreferences 并不保证原子性。相比之下,DataStore 内部会确保数据是在原子读-修改-写操作中更新的。
在迁移支持上:SharedPreferences 没有内置的迁移机制,这就需要您将旧存储中的值重新映射到新存储中,然后进行清理。所有这些都会增加运行时出现异常的几率,因为你很容易遇到数据类型不匹配的问题。然而,DataStore 提供了一种将数据轻松迁移到其中的方法,同时还提供了 SharedPreferences 到 DataStore 迁移的实现方法。
DataStore的使用
DataStore 提供两种不同的实现:Preferences DataStore 和 Proto DataStore。下面分别介绍它们的使用。
Preferences DataStore的使用
第一步添加依赖,代码如下:
implementation("androidx.datastore:datastore-preferences:1.1.1")
第二步,获取 Preferences DataStore 的对象。Google 是推荐使用 preferencesDataStore 属性委托的方式来获取 Preferences DataStore。代码示例如下,如果不了解委托可以看我之前写的文章一文理解 Kotlin 的委托。
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
其委托内部最后是通过 PreferenceDataStoreFactory.create 来创建 Preferences DataStore 的对象。从下面的源码中,我们能看到 preferencesDataStore 属性委托内部保证了线程安全。
public fun preferencesDataStore(
...
): ReadOnlyProperty<Context, DataStore<Preferences>> {
return PreferenceDataStoreSingletonDelegate(name, corruptionHandler, produceMigrations, scope)
}
internal class PreferenceDataStoreSingletonDelegate internal constructor(
...
) : ReadOnlyProperty<Context, DataStore<Preferences>> {
...
@GuardedBy("lock")
@Volatile
private var INSTANCE: DataStore<Preferences>? = null
//双重检查的方式确保线程安全地获取一个对象,但注意这里不是单例
//因为 INSTANCE 不是静态变量
override fun getValue(thisRef: Context, property: KProperty<*>): DataStore<Preferences> {
...
}
}
但是,需要注意的一点是,preferencesDataStore 属性委托不能保证单例对象。如果在同一进程中为给定文件创建多个 DataStore 实例,会破坏所有 DataStore 功能。如果给定文件在同一进程中有多个有效的 DataStore 实例,DataStore 在读取或更新数据时将抛出 IllegalStateException异常。错误代码示例如下:
//会创建三个不同的 DataStore 对象
val dataStore: DataStore<Preferences> by preferencesDataStore("settings")
val dataStore1: DataStore<Preferences> by preferencesDataStore("settings")
val dataStore2: DataStore<Preferences> by preferencesDataStore("settings")
在 Preferences DataStore 中,我们需要为存储在其中的每个值定义一个键(Key)。 Google 提供以下方法用于键值的定义:
- intPreferencesKey()
- doublePreferencesKey()
- stringPreferencesKey()
- booleanPreferencesKey()
- floatPreferencesKey()
- longPreferencesKey()
- stringSetPreferencesKey()
代码示例如下:
//通过 intPreferencesKey 方法创建key
val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
//通过 key 读取内容
val exampleCounterFlow: Flow<Int> = context.dataStore.data
.map { preferences -> // 数据转化
preferences[EXAMPLE_COUNTER] ?: 0
}
//通过 key 写入数据
context.dataStore.edit { settings ->
val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0
settings[EXAMPLE_COUNTER] = currentCounterValue + 1
}
从上面的代码可以看到,获取数据时,由于 DataStore 返回的是 Flow 类型的值,我们就可以使用 Flow 的异常处理方式,代码示例如下:
context.dataStore.data
.catch { exception -> //异常处理
}.map { preferences -> // 数据转化
...
}.collect { // 获取数据
...
}
写入数据时,我们使用 edit 方法。该方法其实是 DataStore 的扩展方法,它内部实现是调用的 updateData 方法。updateData 方法在原子读取-修改-写入操作中事务性地更新数据,这些操作都是串行的,如果某个操作失败的话,事务会终止并抛出异常。
写入完成后,数据最终会存储在 pb 文件中,该文件在 /data/user/0/你的应用包名/files/datastore 目录下,文件名则是设置的名字 + .preferences_pb ,比如上面示例的存储文件名为 settings.preferences_pb
Proto DataStore的使用
相对于 Preferences DataStore ,Proto DataStore 是使用 ProtoBuf 来存储数据。至于什么是 ProtoBuf,你可以理解成更小、更快的Json,更详细的介绍可以看协议缓冲区文档
首先我们添加 Preferences DataStore 的依赖
dependencies {
implementation("androidx.datastore:datastore:1.1.1")
}
Proto DataStore 的使用其实和 Preferences DataStore 类似。不同得是我们需要自己存储对象的结构,以及自定义序列化读写。代码如下所示:首先先定义类对象的结构,
syntax = "proto3";
option java_package = "com.example.application";
option java_multiple_files = true;
message Settings {
int32 example_counter = 1;
}
再使用pb生成对应的类文件,如何生成对应的java类文件可以看"一篇就够"系列:Android 中使用 Protobuf。生成完类之后,我们就可以自定义序列化类 SettingsSerializer,在其中使用 pb 的方式来执行读写操作。代码如下所示:
object SettingsSerializer : Serializer<Settings> {
override val defaultValue: Settings = Settings.getDefaultInstance()
override suspend fun readFrom(input: InputStream): Settings {
try {
//parseFrom 是pb生成的java类中的方法,用于生成对应的类对象
//如果你不想使用pb,可以在这里使用 Gson
//这时就变成了 json 来存储数据了
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 //我们自定义的序列化类
)
最后我们就可以使用 settingsDataStore 来执行读取和写入的操作了。代码示例如下:
//读取数据
val exampleCounterFlow: Flow<Int> = context.settingsDataStore.data
.map { settings ->
settings.exampleCounter
}
//写入数据
suspend fun incrementCounter() {
context.settingsDataStore.updateData { currentSettings ->
currentSettings.toBuilder()
.setExampleCounter(currentSettings.exampleCounter + 1)
.build()
}
}
在多进程中使用
如果你想在多进程中使用 DataStore,你可以使用 MultiProcessDataStoreFactory.create 来获取对应的 DataStore 的对象。在多进程操作中,DataStore 可保证:
- 读取仅返回已持久存储到磁盘的数据。
- 写后读一致性。
- 写入会序列化。
- 写入绝不会阻塞读取。
代码示例如下:
val dataStore: DataStore<Settings> = MultiProcessDataStoreFactory.create(
serializer = SettingsSerializer(),
produceFile = {
File("${context.cacheDir.path}/myapp.preferences_pb")
}
)
如果你看 DataStore 的源码,无论 MultiProcessDataStoreFactory.create 还是 dataStore,以及preferencesDataStore 其功能都是通过 DataStoreImpl 来实现的。就像我们常常使用的 Context,其大部分功能都是 ContextImpl 实现的。这样看,DataStore 是不是瞬间熟悉多了。
回到正题,DataStore 在单进程和多进程的处理其实是分别交给了 SingleProcessCoordinator 和 MultiProcessCoordinator。有兴趣的话,可以自己去看看源码,这里就不多介绍了。
DataStore、MMKV、SharedPreferences的对比
关于的对比,可以看【面试黑洞】Android 的键值对存储有没有最优解这篇文章,讲得很详细了。我这里就总结一下文章的内容,如下面表格所示:
| DataStore | MMKV | SharedPreferences | |
|---|---|---|---|
| 是否支持多进程 | ✅ | ✅ | ❌ |
| 是否可能丢失数据 | ❌ | ✅ | ❌ |
| 存储速度 | 存储速度均衡 | 小数据量很快,大数据量慢 | 小数据量快,大数据量慢 |
| 适用场景 | 非高频写入的场景 | 高频写入场景 | 不使用协程的项目 |