DataStore 介绍
DataStore 提供了一种安全且持久的方式来存储少量数据,例如首选项和应用程序状态。它不支持部分更新:如果任何字段被修改,整个对象将被序列化并持久化到磁盘。如果您想要部分更新,请考虑 Room API (SQLite)。
DataStore 提供 ACID 保证。它是线程安全的,非阻塞的。特别是,它解决了 SharedPreferences API 的这些设计缺陷:
- 同步
API鼓励违反StrictMode apply()和commit()没有发出错误信号的机制apply()将阻塞fsync()上的 UI 线程- 不持久 - 它可以返回尚未持久的状态
- 没有一致性或事务语义
- 在解析错误时引发运行时异常
- 公开对其内部状态的可变引用
DataStore 使用
DataStore 优势:
DataStore 基于事务方式处理数据更新。
DataStore 基于 Kotlin Flow 存取数据,默认在 Dispatchers.IO 里异步操作,避免阻塞 UI 线
程,且在读取数据时能对发生的 Exception 进行处理。
不提供 apply()、commit() 存留数据的方法。
支持 SP 一次性自动迁移至 DataStore 中。
DataStore 对比 MMKV 个人认为优势是基于事务处理,虽然速度不如 MMKV
Preferences DataStore
添加依赖项
implementation 'androidx.datastore:datastore-preferences:1.0.0'
构建 Preferences DataStore
val Context.bookDataStorePf: DataStore<Preferences> by preferencesDataStore(
// 文件名称
name = "pf_datastore")
通过上面的代码,我们就成功创建了 Preferences DataStore ,我们看下 preferencesDataStore() 方法:
/**
* 为单个进程 DataStore 创建属性委托。这应该只在文件中调用一次(在顶层),并且 DataStore 的所有用法
* 都应该使用同一个实例的引用。属性委托的接收者类型必须是Context的实例。
* 这只能在单个进程中的单个类加载器中的单个应用程序中使用。
* 示例用法:
* val Context.myDataStore by preferencesDataStore("filename")
*
* class SomeClass(val context: Context) {
* suspend fun update() = context.myDataStore.edit {...}
* }
* 参数:
* name - 首选项的名称。首选项将存储在应用程序上下文文件目录中“datastore/”子目录中的一个文件中,并使用首选项DataStoreFile 生成。
* corruptionHandler - 如果 DataStore 在尝试读取数据时遇到
* androidx.datastore.core.CorruptionException ,则调用 corruptionHandler。当无法反序列化数据时,序列化程序会抛出 CorruptionExceptions。
* produceMigrations - 产生迁移。 ApplicationContext 作为参数传递给这些回调。 DataMigrations 在对数据进行任何访问之前运行。每个生产者和迁移可能会运行多次,无论它是否已经成功(可能是因为另一个迁移失败或写入磁盘失败。)
* scope - IO 操作和转换函数将执行的范围。
* 返回:
* 将数据存储区作为单例进行管理的属性委托
*/
@Suppress("MissingJvmstatic")
public fun preferencesDataStore(
name: String,
corruptionHandler: ReplaceFileCorruptionHandler<Preferences>? = null,
produceMigrations: (Context) -> List<DataMigration<Preferences>> = { listOf() },
scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
): ReadOnlyProperty<Context, DataStore<Preferences>> {
return PreferenceDataStoreSingletonDelegate(name, corruptionHandler, produceMigrations, scope)
}
上述代码执行后,会在 /data/data/项目包名/files/ 下创建名为 pf_datastore 的文件如下:
可以看到后缀名并不是
xml ,而是 .preferences_pb 。这里需要注意一点:不能将上面的初始化代码
写到 Activity 里面去,否则重复进入 Actvity 并使用 Preferences DataStore 时,会尝试去创建一个同名的 .preferences_pb 文件,因为之前已经创建过一次,当检测到尝试创建同名文件时,会直接抛异常:
java.lang.IllegalStateException: There are multiple DataStores active for the
same file:xxx. You should either maintain your DataStore as a singleton or
confirm that there is no two DataStore's active on the same file (by confirming
that the scope is cancelled).
报错类在 androidx.datastore:datastore-core:1.0.0 的 androidx/datastore/core/SingleProcessDataStore 下:
private val file: File by lazy {
val file = produceFile()
file.absolutePath.let {
synchronized(activeFilesLock) {
check(!activeFiles.contains(it)) {
"There are multiple DataStores active for the same file: $file. You should " +
"either maintain your DataStore as a singleton or confirm that there is " +
"no two DataStore's active on the same file (by confirming that the scope" +
" is cancelled)."
}
activeFiles.add(it)
}
}
file
}
其中 file 是通过 File(applicationContext.filesDir, "datastore/$fileName") 生成的文件,
即 Preferences DataStore 最终要在磁盘中操作的文件地址, activeFiles 是在内存中保存生成的文
件路径的,如果判断到 activeFiles 里已经有该文件,直接抛异常,即不允许重复创建。
存取数据栗子
BookModel.kt 类
data class BookModel(
var name: String = "",
var price: Float = 0f,
var type: Type = Type.ENGLISH
)
enum class Type {
MATH,
CHINESE,
ENGLISH
}
BookRepo.kt 类
const val KEY_BOOK_NAME = "key_book_name"
const val KEY_BOOK_PRICE = "key_book_price"
const val KEY_BOOK_TYPE = "key_book_type"
val Context.bookDataStorePf: DataStore<Preferences> by preferencesDataStore(name = "pf_datastore")
object PreferenceKeys {
val P_KEY_BOOK_NAME = stringPreferencesKey(KEY_BOOK_NAME)
val P_KEY_BOOK_PRICE = floatPreferencesKey(KEY_BOOK_PRICE)
val P_KEY_BOOK_TYPE = stringPreferencesKey(KEY_BOOK_TYPE)
}
// 存储 BookModel 对象
suspend fun Context.saveBookPf(book: BookModel) {
bookDataStorePf.edit { preferences ->
preferences[PreferenceKeys.P_KEY_BOOK_NAME] = book.name
preferences[PreferenceKeys.P_KEY_BOOK_PRICE] = book.price
preferences[PreferenceKeys.P_KEY_BOOK_TYPE] = book.type.name
}
}
// 获取 Flow<BookModel> 对象
suspend fun Context.getBookBf() = bookDataStorePf.data.catch { exception ->
emit(emptyPreferences())
}.map { preferences ->
//对应的Key是 Preferences.Key<T>
val bookName = preferences[PreferenceKeys.P_KEY_BOOK_NAME] ?: ""
val bookPrice = preferences[PreferenceKeys.P_KEY_BOOK_PRICE] ?: 0f
val bookType = Type.valueOf(preferences[PreferenceKeys.P_KEY_BOOK_TYPE] ?: Type.MATH.name)
return@map BookModel(bookName, bookPrice, bookType)
}
TestDataStoreActivity 类
class TestDataStoreActivity : BaseActivity() {
override fun initLayout() = R.layout.activity_test_store
// 点击存储数据
fun onClickSave(v: View) {
runBlocking {
saveBookPf(BookModel("你好", 1f))
}
}
// 点击获取数据
fun onClickRead(v: View) {
runBlocking {
val bookModel = getBookBf().first()
tv_content.text = bookModel.name
}
}
}
通过 bookDataStorePf.data 返回的是 Flow<BookModel> ,那么后续就可以通过 Flow 对数据进行一系列处理。从文件读取数据时,如果出现错误时发出 emptyPreferences()
注意: Preferences DataStore 存取数据时的 Key 是 Preferences.Key<T> 类型,且其中的 T 只能存 Int、Long、Float、Double、Boolean、String、Set< String> 类型,此限制在 androidx/datastore/preferences/core/PreferencesSerializer 类参与序列化的getValueProto() 方法中:
private fun getValueProto(value: Any): Value {
return when (value) {
is Boolean -> Value.newBuilder().setBoolean(value).build()
is Float -> Value.newBuilder().setFloat(value).build()
is Double -> Value.newBuilder().setDouble(value).build()
is Int -> Value.newBuilder().setInteger(value).build()
is Long -> Value.newBuilder().setLong(value).build()
is String -> Value.newBuilder().setString(value).build()
is Set<*> ->
@Suppress("UNCHECKED_CAST")
Value.newBuilder().setStringSet(
StringSet.newBuilder().addAllStrings(value as Set<String>)
).build()
else -> throw IllegalStateException(
"PreferencesSerializer does not support type: ${value.javaClass.name}"
)
}
}
可以看到最后一个 else 逻辑中,如果不是上面的类型,会直接抛异常。因为 Key 是Preferences.Key<T> 类型,系统默认帮我们包了一层,位于 androidx.datastore.preferences.core.PreferencesKeys.kt :
@kotlin.jvm.JvmName public fun booleanPreferencesKey(name: kotlin.String): androidx.datastore.preferences.core.Preferences.Key<kotlin.Boolean> { /* compiled code */ }
@kotlin.jvm.JvmName public fun doublePreferencesKey(name: kotlin.String): androidx.datastore.preferences.core.Preferences.Key<kotlin.Double> { /* compiled code */ }
@kotlin.jvm.JvmName public fun floatPreferencesKey(name: kotlin.String): androidx.datastore.preferences.core.Preferences.Key<kotlin.Float> { /* compiled code */ }
@kotlin.jvm.JvmName public fun intPreferencesKey(name: kotlin.String): androidx.datastore.preferences.core.Preferences.Key<kotlin.Int> { /* compiled code */ }
@kotlin.jvm.JvmName public fun longPreferencesKey(name: kotlin.String): androidx.datastore.preferences.core.Preferences.Key<kotlin.Long> { /* compiled code */ }
@kotlin.jvm.JvmName public fun stringPreferencesKey(name: kotlin.String): androidx.datastore.preferences.core.Preferences.Key<kotlin.String> { /* compiled code */ }
@kotlin.jvm.JvmName public fun stringSetPreferencesKey(name: kotlin.String): androidx.datastore.preferences.core.Preferences.Key<kotlin.collections.Set<kotlin.String>> { /* compiled code */ }
因为上述的声明都在顶层函数中,所以可以直接使用。比如我们想声明一个 String 类型的
Preferences.Key<T> ,可以直接如下进行声明:
val P_KEY_NAME: Preferences.Key<String> = stringPreferencesKey("key")
SP 迁移至 Preferences DataStore
如果想对 SP 进行迁移,只需在 Preferences DataStore 构建环节添加 produceMigrations 参数(该参数含义创建环节已介绍)如下:
//SharedPreference文件名
const val BOOK_PREFERENCES_NAME = "book_preferences"
val Context.bookDataStorePf: DataStore<Preferences> by preferencesDataStore(
name = "pf_datastore", //DataStore文件名称
//将SP迁移到Preference DataStore中
produceMigrations = { context ->
listOf(SharedPreferencesMigration(context, BOOK_PREFERENCES_NAME))
}
)
这样构建完成时, SP 中的内容也会迁移到 Preferences DataStore 中了,注意迁移是一次性的,即执行迁移后,SP 文件会被删除,如下:
迁移之前( SP 文件已存在):
创建
Preferences DataStore 并执行迁移后( SP 文件已经被删除):
Proto DataStore(下一期)
SP 和 Preferences DataStore 的一个缺点是无法定义架构,保证不了存取键时使用了正确的数据类型。Proto DataStore 可利用 Protocol Buffers协议缓冲区 定义架构来解决此问题。