简介

DataStore 是一种数据存储解决方案,允许您使用 ProtoBuf 存储键值对或类型化对象,由 Android 官方在 2020 年推出,并在 2021 年正式发布的库,旨在替代服役多年的 SharedPreferences。

SharedPreferences 的缺陷

  • 文件数据的读取加锁,如果 SharedPreferences 文件未被加载或解析到内存中,读写操作都需要等待,可能会对 UI 线程流畅度造成一定影响,甚至ANR.
  • 在保存数据时,无论是 commit() 还是 apply() 都有可能引发 ANR 问题。
  • 没有错误提示机制。

DataStore 的优点

  • 基于 Flow 实现,保证主线程的安全性。
  • 以事务方式处理更新数据。
  • 可以监听到操作成功或者失败结果。
  • 多进程使用。

Preferences DataStore 和 Proto DataStore

DataStore 提供两种不同的实现:Preferences DataStore 和 Proto DataStore。

  • Preferences DataStore 使用键存储和访问数据。此实现不需要预定义的架构,也不确保类型安全。
  • Proto DataStore 将数据作为自定义数据类型的实例进行存储。此实现要求您使用 ProtoBuf 来定义架构,但可以确保类型安全。

Preferences DataStore

Preferences DataStore 用于存储键值对,相当于 SharedPreferences 的改良版。

dependencies {
    implementation("androidx.datastore:datastore-preferences:1.1.0")
}

实例:

// At the top level of your kotlin file:
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")

读取:

val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
val exampleCounterFlow: Flow<Int> = context.dataStore.data.map { preferences ->
    // No type safety.
    preferences[EXAMPLE_COUNTER] ?: 0
}

支持的类型基本与 SharedPreferences 一致。

写入:

suspend fun incrementCounter() {
    context.dataStore.edit { settings ->
        val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0
        settings[EXAMPLE_COUNTER] = currentCounterValue + 1
    }
}

转换块中的所有代码均被视为单个事务。

Proto DataStore

Proto DataStore 用于存储类型化对象,相当于 SharedPreferences 的升级版。它使用了 Protocol Buffers

dependencies {
    implementation("androidx.datastore:datastore:1.1.0")
}

app/src/main/proto/ 目录下预定义:

// Settings.proto
syntax = "proto3";

option java_package = "com.example.application";
option java_multiple_files = true;

message Settings {
  int32 example_counter = 1;
}

用于告知 DataStore 如何读取和写入数据的序列化器:

object SettingsSerializer : Serializer<Settings> {

    override val defaultValue: Settings = Settings.getDefaultInstance()

    override suspend fun readFrom(input: InputStream): Settings {
        try {
            return Settings.parseFrom(input)
        } catch (e: InvalidProtocolBufferException) {
            throw CorruptionException("Cannot read proto.", e)
        }
    }

    override suspend fun writeTo(t: Settings, output: OutputStream) = t.writeTo(output)
}

实例:

// At the top level of your kotlin file:
val Context.settingsDataStore: DataStore<Settings> by dataStore(
    fileName = "settings.pb",
    serializer = SettingsSerializer
)

读取:

val exampleCounterFlow: Flow<Int> = context.settingsDataStore.data.map { settings ->
    // The exampleCounter property is generated from the proto schema.
    settings.exampleCounter
}

写入:

suspend fun incrementCounter() {
    context.settingsDataStore.updateData {
        it.toBuilder()
            .setExampleCounter(it.exampleCounter + 1)
            .build()
    }
}

同样以事务方式更新数据。

从 SharedPreferences 中迁移

private val Context.migrationDataStore: DataStore<Preferences> by preferencesDataStore(
    name = PREF_FILE_NAME,
    produceMigrations = {
        listOf(SharedPreferencesMigration(it, PREF_FILE_NAME))
    }
)

数据的迁移在创建 DataStore 的过程中自动完成,迁移完成后,原 SharedPreferences 的 XML 文件会被删除。

对比 MMKV

MMKV 是微信团队开源的基于 mmap 内存映射的 Key-Value 组件,底层序列化与反序列化同样使用 ProtoBuf,性能高,稳定性强。

虽然 MMKV 的初衷并不是替代 SharedPreferences,但是同样作为 Key-Value 组件,大多数人都将 MMKV 视为 SharedPreferences 的替代品。

官方文档中也对 MMKV 和 SharedPreferences 的性能进行了对比:

MMKV vs SP

事实上,MMKV 并不是任何时候都更强。由于内存映射这种方案是自行管理一块独立的内存,所以它在尺寸的伸缩上面就比较受限,这就导致它在写大一点的数据时,速度会慢。

另一方面,该写入耗时对于正常开发来说并非特别重要,界面的流畅度更在意主线程的耗时,而 SharedPreferences 本身也提供了异步写入的 API,所以它们都足够快了。但 MMKV 的诞生场景决定了,它更在意同步处理机制下的耗时。

MMKV 还有一个缺陷——丢数据。操作系统在往磁盘写数据的过程中发生意外都会导致文件损坏,这种问题不可避免。MMKV 虽然由于底层机制的原因,在程序崩溃的时候不会影响数据往磁盘的写入,但断电关机之类的操作系统级别的崩溃,MMKV 就没办法了,文件照样会损坏。

对于这种文件损坏,SharedPreferences 和 DataStore 的应对方式是在每次写入新数据之前都对现有文件做一次自动备份,这样在发生了意外出现了文件损坏之后,它们就会把备份的数据恢复过来。

而 MMKV,没有这种自动的备份和恢复,那么当文件发生了损坏,数据就丢了,之前保存的各种信息只能被重置。据官方统计,iOS 微信平均约有 70 万日次的数据校验不通过

因此 MMKV 更适合同步高频写入非重要信息的场景。

参考内容