Jetpack DataStore
是一种数据存储解决方案,可以和 SharedPreferences
一样存储键值对,还可以用 Protocol Buffers
协议来存储类型化对象数据。DataStore
使用 Kotlin Flow
异步地存储数据。Android 官方推荐我们把 SharedPreferencs 迁移到 DataStore 。
DataStore 提供了两种不同的实现:Preferences DataStore
和 Proto DataStore
。
Preference DataStore | Proto DataStore | |
---|---|---|
数据类型 | 键值对 | 自定义数据类型 |
类型安全 | 否 | 是 |
需要预定义数据结构 | 否 | 是 |
异步操作:DataStore 提供了异步读写操作,使用 Kotlin 协程和 Flow 来处理数据的访问,而且默认会有用一个 IO 协程分发器来执行 IO 读写操作,从而提高应用程序的响应性能,不会出现 SharedPreferences 那种忘了在子线程读取数据,导致阻塞了主线程的问题
类型安全:DataStore 允许使用类型化的对象(ProtoBuf)来存储和检索数据,避免了在 SharedPreferences
中进行手动的强制类型转换。这样可以减少由于类型错误而引起的潜在 bug,并提供更可靠的代码
跨进程支持:如果应用程序需要在多个进程中共享数据,DataStore 提供了 MultiProcessDataStore
的实现,可以安全地在多个进程之间共享数据
更好的性能:DataStore 用的是 Protocol Buffers 来序列化数据,存储的偏好数据是 pb 文件,而 SharedPreferences 则将数据存储在 XML 文件中,ProtoBuf 相对于 XML
具有更好的性能。因为它提供了更小的尺寸、更快的序列化和反序列化速度,以及更低的处理开销
兼容性和迁移性:DataStore 提供了一种平滑迁移的方式,可以方便地替换现有的 SharedPreferences 为 DataStore,可以选择将部分或全部数据迁移到 DataStore 中
异常捕获:对于 SharedPreferences ,如果在调用 apply()
或 commit()
方法后发生了写入磁盘的错误(例如由于设备存储空间不足或其他原因),开发者将无法立即获得通知或处理相应的错误情况。而 DataStore 提供了 CorruptionHandler
,在序列化数据失败的时候,就会把异常交给该 Handler 处理。
特征 | DataStore | SharedPreferences |
---|---|---|
数据存储方式 | 使用协议缓冲区(Protocol Buffers)进行序列化 | 使用 XML 文件进行存储 |
数据类型支持 | 支持原始数据类型、可空类型、集合类型等 | 仅支持基本数据类型和字符串 |
数据迁移和清除 | 提供数据迁移工具和清除功能 | 需要手动处理数据迁移和清除 |
多进程访问 | 支持多进程访问 | 不支持多进程访问 |
持续监听数据变化 | 不支持 | 支持 |
1、在同一个进程中,不要为给定的文件创建多个 DataStore 实例
如果在同一个进程中有多个活动的 DataStore 与同一个文件关联,当读取或更新数据时,DataStore 会抛出非法状态异常(IllegalStateException)
。
当你创建一个 DataStore 实例时,它会与特定的文件进行关联,并负责管理该文件中的数据。如果在同一个进程中创建多个与同一文件关联的 DataStore 实例,它们会共享相同的文件,并尝试并发地读取和更新其中的数据。
这样的并发访问可能会导致数据不一致或冲突。例如,一个 DataStore 实例可能正在向文件写入数据,而另一个实例尝试读取相同的数据,这可能导致无法预料的结果或错误的数据返回。
为了避免这种情况,强烈建议在同一个进程中只创建一个与给定文件关联的 DataStore 实例。通过单一实例操作数据可以确保数据的一致性和正确性。
如果需要在应用程序中的不同组件之间共享数据,可以使用依赖注入(如 Dagger、Koin 等)或其他合适的方式来提供同一实例的 DataStore。
2、DataStore 的泛型类型必须是不可变的
对于在 DataStore 中使用的类型进行变更会使 DataStore
提供的任何保证失效,并且可能产生严重且难以捕获的错误。
3、不要将 SingleProcessDataStore
和 MultiProcessDataStore
混合使用于同一个文件:如果打算从多个进程中访问 DataStore,就要一直使用 MultiProcessDataStore
。
原因如下:
线程安全性问题:SingleProcessDataStore 是为单进程设计的,它在处理数据时没有考虑到多个进程同时进行读写操作的情况。如果在多个进程中同时使用 SingleProcessDataStore 访问同一个文件,可能会导致数据不一致或竞态条件等线程安全性问题。
数据冲突:SingleProcessDataStore 和 MultiProcessDataStore 使用不同的数据存储机制和锁定策略。当混合使用时,可能会导致数据冲突和意外的行为。例如,一个进程通过 SingleProcessDataStore 向文件写入数据,而另一个进程通过 MultiProcessDataStore 尝试读取相同的数据,会出现读取到部分更新数据或无法正确读取的情况。
功能不兼容:SingleProcessDataStore 和 MultiProcessDataStore 具有不同的特性和限制。SingleProcessDataStore 支持更高级别的事务性操作(例如端到端可靠性),而 MultiProcessDataStore 提供了多进程间共享数据的能力。将它们混合使用可能导致功能不兼容或无法预期的结果。
基于以上原因,如果打算从多个进程中访问 DataStore,并且需要保证数据的一致性和正确性,应始终使用 MultiProcessDataStore。它专为多进程场景设计,提供了适当的锁定机制和数据同步方式,以确保多个进程间对数据的安全访问和更新。
接下来讲解的源码是基于 DataStore 1.0.0 版本的,截止到我写这篇文章的日期时,最新的版本是 1.1.0-alpha04 。
Preferences Store:
def DATA_STORE_VERSION = "1.0.0"
// Preferences DataStore (类似于 SharedPreferences 的 API)
dependencies {
implementation "androidx.datastore:datastore-preferences:$DATA_STORE_VERSION"
// 可选 - RxJava2 支持
implementation "androidx.datastore:datastore-preferences-rxjava2:1.0.0"
// 可选 - RxJava3 支持
implementation "androidx.datastore:datastore-preferences-rxjava3:1.0.0"
// 可选 - 多进程依赖
implementation "androidx.datastore:datastore-core-android:1.1.0-alpha04"
}
// 或者 - 使用以下不带 Android 依赖的库
dependencies {
implementation "androidx.datastore:datastore-preferences-core:$DATA_STORE_VERSION"
}
Proto DataStore:
def DATA_STORE_VERSION = "1.0.0"
// 类型化的 DataStore (包含类型化 API,例如 Proto)
dependencies {
implementation "androidx.datastore:datastore:$DATA_STORE_VERSION"
// 可选 - RxJava2 支持
implementation "androidx.datastore:datastore-rxjava2:1.0.0"
// 可选 - RxJava3 支持
implementation "androidx.datastore:datastore-rxjava3:1.0.0"
// 可选 - 多进程依赖
implementation "androidx.datastore:datastore-core-android:1.1.0-alpha04"
}
// 或者 - 使用以下不带 Android 依赖的库
dependencies {
implementation "androidx.datastore:datastore-core:$DATA_STORE_VERSION"
}
注意:如果您在使用 datastore-preferences-core
库时使用了 Proguard
,则必须在 proguard-rules.pro
文件中手动添加 Proguard 规则(https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:datastore/datastore-preferences/proguard-rules.pro),以防止数据被混淆后无法读出来。
首先,使用由 preferencesDataStore
创建的委托对象(PreferenceDataStoreSingletonDelegate)来创建 Datastore
的实例。推荐在 Kotlin 文件的顶层调用它一次,这样可以更容易地将 DataStore 保持为单例。如果您使用 RxJava,还可以使用 RxPreferenceDataStoreBuilder
。必填的 name
参数是保存 Preferences DataStore
数据的文件名前缀。
// 在 Kotlin 文件的顶层处:
val Context.dataStore by preferencesDataStore(name = "settings")
由于 Preferences DataStore 不支持类型化的数据(Protocol Buffers)预定义的数据结构(Schema),要使用相应的键类型函数为需要存储在 DataStore
实例中的每个值定义一个键。例如,要为 int
值定义一个键,就要用使用 intPreferencesKey()
。然后,使用 DataStore.data
属性通过 Flow
获取存储值。
// 初始化计数器偏好值的 Key
val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
// 用于收集偏好值的数据流
val exampleCounterFlow: Flow = context.dataStore.data
.map { preferences ->
// 非类型安全
preferences[EXAMPLE_COUNTER] ?: 0
}
// 获取 SafeFlow 发射的元素
val exampleCounter = exampleCounterFlow.firstOrNull()
在 Protocol Buffers(简称ProtoBuf)中,Schema 指的是定义数据结构和消息格式的文件
。它描述了如何组织和序列化数据,并为不同编程语言提供了一致的接口和规范。
可以将Schema视为一种数据模型或约定,用于定义消息的字段、类型和顺序。通过使用特定的语法,开发人员可以定义消息的结构和字段属性,包括字段名称、数据类型、标识符等。随后,ProtoBuf编译器可以根据Schema文件生成对应的源代码,供开发人员在各种编程语言中使用。
Preferences DataStore
提供了一个 edit()
函数,用于以事务的方式更新 DataStore 中的数据。该函数的 transform
参数是代码块,在该代码块中可以根据需要更新值。transform
代码块中的所有代码都是一个单独的事务。
suspend fun incrementCounter() {
context.dataStore.edit { settings ->
val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0
// 更新计数器的偏好(Preference)值
settings[EXAMPLE_COUNTER] = currentCounterValue + 1
}
}
PreferencesDataStore
的使用一般是先在 Kotlin 文件的顶部声明一个 dataStore
,该全局变量的值是 preferencesDataStore()
方法提供的,该方法会创建一个委托对象 PreferencesDataStoreSingletonDelegate
,该委托类是一个只读属性,当使用 dataStore
时,就会调用该只读属性的 getValue()
方法,然后才会开始真正的初始化过程,主要的操作就是执行迁移(runMigrations)和创建存储偏好数据的文件,最后创建一个 PreferencesDataStore 对象,该对象会持有一个单进程数据存储委托类 SingleProcessDataStore
。
PreferencesDataStore
和 SingleProcessDataStore
都实现了 DataStore
接口,PreferencesDataStore 实现了 DataStore 接口的 updateData
方法,但是具体的实现是委托给了 SingleProcessDataStore
来做,PreferencesDataStore Store 只是做了一下冻结(freeze)的操作,freeze 是 MutablePrerferences
中的一个方法,冻结操作的实现后面会讲到。
@Suppress("MissingJvmstatic")
public fun preferencesDataStore(
name: String,
corruptionHandler: ReplaceFileCorruptionHandler? = null,
produceMigrations: (Context) -> List> = { listOf() },
scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
): ReadOnlyProperty> {
return PreferenceDataStoreSingletonDelegate(name, corruptionHandler, produceMigrations, scope)
}
preferenceDataStore()
方法用于创建一个单进程 DataStore 的属性委托 PreferenceDataStoreSingletonDelegate
。这个方法应该只在文件的顶层调用一次,所有使用 DataStore 的地方都应该引用相同的实例。属性委托的接收者类型必须是 Context
的实例。
参数:
name
:preference的名称。偏好设置将存储在应用程序上下文文件目录的 datastore/
子目录下,使用 preferencesDataStoreFile()
方法生成
corruptionHandler
:序列化失败处理器,在 DataStore 在尝试读取数据时遇到 CorruptionException
时会对异常状况进行处理
produceMigrations
:生成迁移的函数。会用ApplicationContext
作为参数传递给该回调函数。DataMigrations 在可以访问任何数据之前运行。每个生产者和迁移可能会多次运行,无论其是否已成功(可能是因为另一个迁移失败或写入磁盘失败)
scope
:执行 IO 操作和转换函数的协程上下文,默认是有 SupervisorJob
和 IO 协程分发器的协程作用域。
/**
* 用于管理 Preferences DataStore 的单例委托类
*/
internal class PreferenceDataStoreSingletonDelegate internal constructor(
private val name: String,
private val corruptionHandler: ReplaceFileCorruptionHandler?,
private val produceMigrations: (Context) -> List>,
private val scope: CoroutineScope
) : ReadOnlyProperty> {
// 初始化锁对象
private val lock = Any()
@GuardedBy("lock")
@Volatile
private var INSTANCE: DataStore? = null
/**
* 获取 DataStore 实例(通过 by 关键字)
*/
override fun getValue(thisRef: Context, property: KProperty<*>): DataStore {
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!!
}
}
}
PreferenceDataStoreSingletonDelegate
实现了 ReadOnlyProperty
接口,也就是它是一个只读属性,该接口只有一个 getValue
方法。
getValue
方法中调用了 PreferenceDataStoreFactory
的 create()
方法来创建委托类的单例,并且调用了 produceMigrations
方法和 preferencesDataStoreFile
方法来执行迁移的工作和创建保存偏好值的 Preferences 文件。
比如下面这段通过 by 关键字获取 PreferenceDataStore
实例的代码:
val Context.dataStore by preferencesDataStore(name = "settings")
getValue
的调用时机是在 dataStore 初始化的时候,比如声明的 Context.dataStore
字段在反编译后的 Java 代码如下,getDataStore()
中,调用的就是只读属性 ReadOnlyProperty 的 getValue
方法。
// ...
public final class MainDataStoreKt {
// $FF: synthetic field
static final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)Reflection.property1(new PropertyReference1Impl(MainDataStoreKt.class, "dataStore", "getDataStore(Landroid/content/Context;)Landroidx/datastore/core/DataStore;", 1))};
@NotNull
private static final ReadOnlyProperty dataStore$delegate = PreferenceDataStoreDelegateKt.preferencesDataStore$default("settings", (ReplaceFileCorruptionHandler)null, (Function1)null, (CoroutineScope)null, 14, (Object)null);
@NotNull
public static final DataStore getDataStore(@NotNull Context $this$dataStore) {
Intrinsics.checkNotNullParameter($this$dataStore, "$this$dataStore");
return (DataStore)dataStore$delegate.getValue($this$dataStore, $$delegatedProperties[0]);
}
}
字段和方法说明:
$$delegatedProperties
:合成字段,用于委托属性的存储和访问。
dataStore$delegate
:主数据存储的委托属性,通过调用preferencesDataStore$default
函数获取。
getDataStore
方法:通过传入Context参数获取主数据存储的DataStore对象。
整个代码主要用于获取主数据存储的 DataStore 对象,并提供一个对外访问的接口。
/**
* 根据提供的上下文和名称生成 Preferences DataStore 的 File 对象。
* 该文件位于 [this.applicationContext.filesDir] + "datastore/" 子目录中,名称为 [name]。
* 这是公开的,以便进行测试和向后兼容(例如,从 `preferencesDataStore` 委托
* 或 context.createDataStore 迁移到 PreferencesDataStoreFactory)。
*
* 请勿在 DataStore 外部使用此文件。
*
* @this 应用程序的上下文,用于获取文件目录
* @name 偏好设置的名称
*/
public fun Context.preferencesDataStoreFile(name: String): File =
this.dataStoreFile("$name.preferences_pb")
/**
* 根据提供的上下文和名称生成 DataStore 的 File 对象。文件通过调用 `File(context.applicationContext.filesDir, "datastore/$fileName")` 生成。
* 这是公开的,以便进行测试和向后兼容(例如,从 `dataStore` 委托或 context.createDataStore 迁移到 DataStoreFactory)。
*
* 请勿在 DataStore 外部使用此文件。
*
* @this 应用程序的上下文,用于获取文件目录
* @fileName 文件名
*/
public fun Context.dataStoreFile(fileName: String): File =
File(applicationContext.filesDir, "datastore/$fileName")
preferencesDataStoreFile
方法只是简单地通过 dataStoreFile
方法,在 datastore 目录下创建对应的偏好数据文件,文件的后缀是 name
+ .preferences.pb
。
public object PreferenceDataStoreFactory {
@JvmOverloads
public fun create(
corruptionHandler: ReplaceFileCorruptionHandler? = null,
migrations: List> = listOf(),
scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
produceFile: () -> File
): DataStore {
// 创建委托对象
val delegate = DataStoreFactory.create(
serializer = PreferencesSerializer,
corruptionHandler = corruptionHandler,
migrations = migrations,
scope = scope
) {
val file = produceFile()
check(file.extension == PreferencesSerializer.fileExtension) {
"文件:$file 的扩展名与 Preferences 文件所需的扩展名 ${PreferencesSerializer.fileExtension} 不匹配"
}
file
}
return PreferenceDataStore(delegate)
}
}
在 PreferenceDataStoreFactory
的 create() 方法中,会通过 DataStoreFactory 创建 PreferencesDataStore
实例,这个方法是公开的,也就是除了用 preferencesDataStore
,我们也可以直接用 PreferencesDataStoreFactory.create
来创建 DataStore 。
方法参数说明:
corruptionHandler
:当读取数据时遇到 CorruptionException
异常时,会调用该处理程序进行处理。
migrations
:在访问数据之前运行的迁移操作列表。
scope
:用于执行 IO 操作和转换函数的协程作用域。
produceFile
:用于返回 DataStore
将要操作的文件的函数。
该方法首先使用提供的参数创建一个 DataStoreFactory
委托对象,并将其传递给 PreferenceDataStore
的构造函数,最终返回一个新的 DataStore
实例。在创建 DataStoreFactory
委托对象时,会验证文件的扩展名是否与要求的扩展名匹配。
要注意的是将 DataStore
实例作为单例,不要在多个文件中声明相同文件的 dataStore 变量,以确保正确的功能和数据的一致性。
/**
* DataStore工厂类,用于创建DataStore实例。
*/
public object DataStoreFactory {
/**
* 创建DataStore实例的公共工厂方法。
*/
@JvmOverloads // 为Java用户生成默认参数的构造函数
public fun create(
serializer: Serializer,
corruptionHandler: ReplaceFileCorruptionHandler? = null,
migrations: List> = listOf(),
scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
produceFile: () -> File
): DataStore =