DataStore 使用

927 阅读5分钟

DataStore 介绍

DataStore 提供了一种安全且持久的方式来存储少量数据,例如首选项和应用程序状态。它不支持部分更新:如果任何字段被修改,整个对象将被序列化并持久化到磁盘。如果您想要部分更新,请考虑 Room API (SQLite)

DataStore 提供 ACID 保证。它是线程安全的,非阻塞的。特别是,它解决了 SharedPreferences API 的这些设计缺陷:

  1. 同步 API 鼓励违反 StrictMode
  2. apply()commit() 没有发出错误信号的机制
  3. apply() 将阻塞 fsync() 上的 UI 线程
  4. 不持久 - 它可以返回尚未持久的状态
  5. 没有一致性或事务语义
  6. 在解析错误时引发运行时异常
  7. 公开对其内部状态的可变引用

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 的文件如下:

image.png 可以看到后缀名并不是 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.0androidx/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 存取数据时的 KeyPreferences.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 逻辑中,如果不是上面的类型,会直接抛异常。因为 KeyPreferences.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 文件已存在):

image.png 创建 Preferences DataStore 并执行迁移后( SP 文件已经被删除):

image.png

Proto DataStore(下一期)

SPPreferences DataStore 的一个缺点是无法定义架构,保证不了存取键时使用了正确的数据类型。Proto DataStore 可利用 Protocol Buffers协议缓冲区 定义架构来解决此问题。