探索 Jetpack PreferenceDataStore 原理

前言

什么是 DataStore ?

Jetpack DataStore 是一种数据存储解决方案,可以和 SharedPreferences 一样存储键值对,还可以用 Protocol Buffers 协议来存储类型化对象数据。DataStore 使用 Kotlin Flow 异步地存储数据。Android 官方推荐我们把 SharedPreferencs 迁移到 DataStore 。

Preference DataStore 与 Proto DataStore

DataStore 提供了两种不同的实现:Preferences DataStoreProto DataStore

Preference DataStore Proto DataStore
数据类型 键值对 自定义数据类型
类型安全
需要预定义数据结构

DataStore 的优势

  1. 异步操作:DataStore 提供了异步读写操作,使用 Kotlin 协程和 Flow 来处理数据的访问,而且默认会有用一个 IO 协程分发器来执行 IO 读写操作,从而提高应用程序的响应性能,不会出现 SharedPreferences 那种忘了在子线程读取数据,导致阻塞了主线程的问题

  2. 类型安全:DataStore 允许使用类型化的对象(ProtoBuf)来存储和检索数据,避免了在 SharedPreferences 中进行手动的强制类型转换。这样可以减少由于类型错误而引起的潜在 bug,并提供更可靠的代码

  3. 跨进程支持:如果应用程序需要在多个进程中共享数据,DataStore 提供了 MultiProcessDataStore 的实现,可以安全地在多个进程之间共享数据

  4. 更好的性能:DataStore 用的是 Protocol Buffers 来序列化数据,存储的偏好数据是 pb 文件,而 SharedPreferences 则将数据存储在 XML 文件中,ProtoBuf 相对于 XML 具有更好的性能。因为它提供了更小的尺寸、更快的序列化和反序列化速度,以及更低的处理开销

  5. 兼容性和迁移性:DataStore 提供了一种平滑迁移的方式,可以方便地替换现有的 SharedPreferences 为 DataStore,可以选择将部分或全部数据迁移到 DataStore 中

  6. 异常捕获:对于 SharedPreferences ,如果在调用 apply()commit() 方法后发生了写入磁盘的错误(例如由于设备存储空间不足或其他原因),开发者将无法立即获得通知或处理相应的错误情况。而 DataStore 提供了 CorruptionHandler,在序列化数据失败的时候,就会把异常交给该 Handler 处理。

特征 DataStore SharedPreferences
数据存储方式 使用协议缓冲区(Protocol Buffers)进行序列化 使用 XML 文件进行存储
数据类型支持 支持原始数据类型、可空类型、集合类型等 仅支持基本数据类型和字符串
数据迁移和清除 提供数据迁移工具和清除功能 需要手动处理数据迁移和清除
多进程访问 支持多进程访问 不支持多进程访问
持续监听数据变化 不支持 支持

正确使用 DataStore 的规则

1、在同一个进程中,不要为给定的文件创建多个 DataStore 实例

如果在同一个进程中有多个活动的 DataStore 与同一个文件关联,当读取或更新数据时,DataStore 会抛出非法状态异常(IllegalStateException)

当你创建一个 DataStore 实例时,它会与特定的文件进行关联,并负责管理该文件中的数据。如果在同一个进程中创建多个与同一文件关联的 DataStore 实例,它们会共享相同的文件,并尝试并发地读取和更新其中的数据。

这样的并发访问可能会导致数据不一致或冲突。例如,一个 DataStore 实例可能正在向文件写入数据,而另一个实例尝试读取相同的数据,这可能导致无法预料的结果或错误的数据返回。

为了避免这种情况,强烈建议在同一个进程中只创建一个与给定文件关联的 DataStore 实例。通过单一实例操作数据可以确保数据的一致性和正确性。

如果需要在应用程序中的不同组件之间共享数据,可以使用依赖注入(如 Dagger、Koin 等)或其他合适的方式来提供同一实例的 DataStore。

2、DataStore 的泛型类型必须是不可变的

对于在 DataStore 中使用的类型进行变更会使 DataStore 提供的任何保证失效,并且可能产生严重且难以捕获的错误。

3、不要将 SingleProcessDataStoreMultiProcessDataStore 混合使用于同一个文件 :如果打算从多个进程中访问 DataStore,就要一直使用 MultiProcessDataStore

原因如下:

  • 线程安全性问题:SingleProcessDataStore 是为单进程设计的,它在处理数据时没有考虑到多个进程同时进行读写操作的情况。如果在多个进程中同时使用 SingleProcessDataStore 访问同一个文件,可能会导致数据不一致或竞态条件等线程安全性问题。

  • 数据冲突:SingleProcessDataStore 和 MultiProcessDataStore 使用不同的数据存储机制和锁定策略。当混合使用时,可能会导致数据冲突和意外的行为。例如,一个进程通过 SingleProcessDataStore 向文件写入数据,而另一个进程通过 MultiProcessDataStore 尝试读取相同的数据,会出现读取到部分更新数据或无法正确读取的情况。

  • 功能不兼容:SingleProcessDataStore 和 MultiProcessDataStore 具有不同的特性和限制。SingleProcessDataStore 支持更高级别的事务性操作(例如端到端可靠性),而 MultiProcessDataStore 提供了多进程间共享数据的能力。将它们混合使用可能导致功能不兼容或无法预期的结果。

基于以上原因,如果打算从多个进程中访问 DataStore,并且需要保证数据的一致性和正确性,应始终使用 MultiProcessDataStore。它专为多进程场景设计,提供了适当的锁定机制和数据同步方式,以确保多个进程间对数据的安全访问和更新。

一、PreferencesDataStore 用法

添加 Gradle 依赖

接下来讲解的源码是基于 DataStore 1.0.0 版本的,截止到我写这篇文章的日期时,最新的版本是 1.1.0-alpha04 。

Preferences Store:

Groovy 复制代码
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:

Groovy 复制代码
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 规则(cs.android.com/androidx/pl...),以防止数据被混淆后无法读出来。

创建 Preferences DataStore

首先,使用由 preferencesDataStore 创建的委托对象(PreferenceDataStoreSingletonDelegate)来创建 Datastore<Preferences> 的实例。推荐在 Kotlin 文件的顶层调用它一次,这样可以更容易地将 DataStore 保持为单例。如果您使用 RxJava,还可以使用 RxPreferenceDataStoreBuilder。必填的 name 参数是保存 Preferences DataStore 数据的文件名前缀。

Kotlin 复制代码
// 在 Kotlin 文件的顶层处:
val Context.dataStore by preferencesDataStore(name = "settings")

从 Preference DataStore 中读取数据

由于 Preferences DataStore 不支持类型化的数据(Protocol Buffers)预定义的数据结构(Schema),要使用相应的键类型函数为需要存储在 DataStore<Preferences> 实例中的每个值定义一个键。例如,要为 int 值定义一个键,就要用使用 intPreferencesKey()。然后,使用 DataStore.data 属性通过 Flow 获取存储值。

Kotlin 复制代码
// 初始化计数器偏好值的 Key
val EXAMPLE_COUNTER = intPreferencesKey("example_counter")

// 用于收集偏好值的数据流
val exampleCounterFlow: Flow<Int> = context.dataStore.data
  .map { preferences ->
    // 非类型安全
    preferences[EXAMPLE_COUNTER] ?: 0
}
  
// 获取 SafeFlow 发射的元素
val exampleCounter = exampleCounterFlow.firstOrNull()

Protocol Buffers Schema

在 Protocol Buffers(简称ProtoBuf)中,Schema 指的是定义数据结构和消息格式的文件。它描述了如何组织和序列化数据,并为不同编程语言提供了一致的接口和规范。

可以将Schema视为一种数据模型或约定,用于定义消息的字段、类型和顺序。通过使用特定的语法,开发人员可以定义消息的结构和字段属性,包括字段名称、数据类型、标识符等。随后,ProtoBuf编译器可以根据Schema文件生成对应的源代码,供开发人员在各种编程语言中使用。

往 Preference DataStore 写数据

Preferences DataStore 提供了一个 edit() 函数,用于以事务的方式更新 DataStore 中的数据。该函数的 transform 参数是代码块,在该代码块中可以根据需要更新值。transform 代码块中的所有代码都是一个单独的事务。

Kotlin 复制代码
suspend fun incrementCounter() {
  context.dataStore.edit { settings ->
    val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0
    
    // 更新计数器的偏好(Preference)值
    settings[EXAMPLE_COUNTER] = currentCounterValue + 1
  }
}

1. Preference DataStore 创建过程

PreferencesDataStore 的使用一般是先在 Kotlin 文件的顶部声明一个 dataStore ,该全局变量的值是 preferencesDataStore() 方法提供的,该方法会创建一个委托对象 PreferencesDataStoreSingletonDelegate ,该委托类是一个只读属性,当使用 dataStore 时,就会调用该只读属性的 getValue() 方法,然后才会开始真正的初始化过程,主要的操作就是执行迁移(runMigrations)和创建存储偏好数据的文件,最后创建一个 PreferencesDataStore 对象,该对象会持有一个单进程数据存储委托类 SingleProcessDataStore

PreferencesDataStoreSingleProcessDataStore 都实现了 DataStore 接口,PreferencesDataStore 实现了 DataStore 接口的 updateData 方法,但是具体的实现是委托给了 SingleProcessDataStore 来做,PreferencesDataStore Store 只是做了一下冻结(freeze)的操作,freeze 是 MutablePrerferences中的一个方法,冻结操作的实现后面会讲到。

preferencesDataStore

Kotlin 复制代码
@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)
}

preferenceDataStore() 方法用于创建一个单进程 DataStore 的属性委托 PreferenceDataStoreSingletonDelegate。这个方法应该只在文件的顶层调用一次,所有使用 DataStore 的地方都应该引用相同的实例。属性委托的接收者类型必须是 Context 的实例。

参数:

  • name:preference的名称。偏好设置将存储在应用程序上下文文件目录的 datastore/ 子目录下,使用 preferencesDataStoreFile() 方法生成

  • corruptionHandler:序列化失败处理器,在 DataStore 在尝试读取数据时遇到 CorruptionException 时会对异常状况进行处理

  • produceMigrations:生成迁移的函数。会用ApplicationContext 作为参数传递给该回调函数。DataMigrations 在可以访问任何数据之前运行。每个生产者和迁移可能会多次运行,无论其是否已成功(可能是因为另一个迁移失败或写入磁盘失败)

  • scope:执行 IO 操作和转换函数的协程上下文,默认是有 SupervisorJob 和 IO 协程分发器的协程作用域。

PreferenceDataStoreSingletonDelegate

Kotlin 复制代码
/**
 * 用于管理 Preferences DataStore 的单例委托类
 */
internal class PreferenceDataStoreSingletonDelegate internal constructor(
    private val name: String,
    private val corruptionHandler: ReplaceFileCorruptionHandler<Preferences>?,
    private val produceMigrations: (Context) -> List<DataMigration<Preferences>>,
    private val scope: CoroutineScope
) : ReadOnlyProperty<Context, DataStore<Preferences>> {

    // 初始化锁对象
    private val lock = Any()

    @GuardedBy("lock")
    @Volatile
    private var INSTANCE: DataStore<Preferences>? = null

    /**
     * 获取 DataStore 实例(通过 by 关键字)
     */
    override fun getValue(thisRef: Context, property: KProperty<*>): DataStore<Preferences> {
        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 方法中调用了 PreferenceDataStoreFactorycreate() 方法来创建委托类的单例,并且调用了 produceMigrations 方法和 preferencesDataStoreFile 方法来执行迁移的工作和创建保存偏好值的 Preferences 文件。

比如下面这段通过 by 关键字获取 PreferenceDataStore 实例的代码:

Kotlin 复制代码
val Context.dataStore by preferencesDataStore(name = "settings")

getValue 的调用时机是在 dataStore 初始化的时候,比如声明的 Context.dataStore 字段在反编译后的 Java 代码如下,getDataStore() 中,调用的就是只读属性 ReadOnlyProperty 的 getValue 方法。

Java 复制代码
// ...
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 对象,并提供一个对外访问的接口。

创建 PreferencesDataStore 文件

Kotlin 复制代码
/**
 * 根据提供的上下文和名称生成 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

PreferenceDataStoreFactory#create

Kotlin 复制代码
public object PreferenceDataStoreFactory {

    @JvmOverloads
    public fun create(
        corruptionHandler: ReplaceFileCorruptionHandler<Preferences>? = null,
        migrations: List<DataMigration<Preferences>> = listOf(),
        scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
        produceFile: () -> File
    ): DataStore<Preferences> {
        // 创建委托对象
        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 变量,以确保正确的功能和数据的一致性。

DataStoreFactory#create

Kotlin 复制代码
/**
 * DataStore工厂类,用于创建DataStore实例。
 */
public object DataStoreFactory {
  
    /**
     * 创建DataStore实例的公共工厂方法。
     */
    @JvmOverloads // 为Java用户生成默认参数的构造函数
    public fun <T> create(
        serializer: Serializer<T>,
        corruptionHandler: ReplaceFileCorruptionHandler<T>? = null,
        migrations: List<DataMigration<T>> = listOf(),
        scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
        produceFile: () -> File
    ): DataStore<T> =
        // 创建在单进程中使用的 DataStore
        SingleProcessDataStore(
            produceFile = produceFile,
            serializer = serializer,
            corruptionHandler = corruptionHandler ?: NoOpCorruptionHandler(),
            initTasksList = listOf(DataMigrationInitializer.getInitializer(migrations)), // 初始化任务列表
            scope = scope
        )
}

DataStoreFactory 包含一个名为 create 的静态方法,用于创建 DataStore 的实例。这个方法接受一系列参数,并返回一个 DataStore<T> 对象。

create() 方法内部创建了一个 SingleProcessDataStore 的实例,并传递了相应的参数。其中,initTasksList 参数使用了 DataMigrationInitializer.getInitializer(migrations) 获取初始化任务列表。

通过调用 DataStoreFactory.create 方法,可以方便地创建一个 DataStore 实例,并进行一些配置,例如指定序列化器、处理数据损坏的方式、添加数据迁移操作等。

DataStore

Kotlin 复制代码
/**
 * DataStore 提供了一种安全和可靠的方式来存储小量数据,例如偏好设置和应用程序状态。
 * 它不支持部分更新:如果修改任何字段,整个对象将被序列化并持久化到磁盘上。
 * 如果需要对数据进行部分更新,请考虑使用Room API(SQLite)
 */
public interface DataStore<T> {
    /**
     * 提供高效、缓存(可能时)的访问最新的持久化状态。
     * Flow要么发出一个值,要么在尝试从磁盘读取时抛出异常。如果遇到异常,在再次收集时将尝试重新读取数据。
     *
     * 不要在此API之上添加缓存层:无法保证一致性。相反,使用data.first()来访问单个快照。
     *
     * @return 表示数据当前状态的Flow
     * @throws IOException 在读取数据时遇到异常时抛出
     */
    public val data: Flow<T>

    /**
     * 在原子读取-修改-写入操作中事务性地更新数据。所有操作都是串行化的。
     *
     * 当数据被持久化到磁盘上(之后[data]将反映更新)时,协程完成。
     * 如果transform或写入磁盘失败,则事务将被中止并抛出异常。
     *
     * @return transform返回的快照
     * @throws IOException 在将数据写入磁盘时遇到异常时抛出
     * @throws Exception 由transform函数抛出的异常
     */
    public suspend fun updateData(transform: suspend (t: T) -> T): T
}

DataStore 中的 data 字段在读取数据的时候,如果出现了异常,是会抛出异常的,这个在使用的时候要注意,可以自定义一个带了 CoroutineExceptionHandler 的协程作用域(CoroutineScope)来避免这个问题,通过 data.first() 可以访问数据的快照,data 的类型是 Flow ,通过该数据流可以收集 DataStore 中的数据。

除了 data 字段,DataStore 还声明了一个 updateData 方法,该方法用于在原子读取-修改-写入操作中事务性地更新数据,这些操作都是串行的,如果某个操作失败的话,事务会终止并抛出异常。在 transform 代码块是一个挂起函数,可以在里面执行耗时操作。需要特别注意的是,这个方法也是有可能会抛出异常的,所以要捕获或者使用协程异常处理器(CoroutineExceptionHandler)。

二、SafeFlow

由于 SingleProcessDataStore 使用到了冷流 SafeFlow和热流 MutableStateFlow ,所以下面也简单介绍一下相关的概念和实现。

SafeFlow

Kotlin 复制代码
public fun <T> flow(
  @BuilderInference block: suspend FlowCollector<T>.() -> Unit
): Flow<T> = SafeFlow(block)

// 命名匿名对象
private class SafeFlow<T>(
  private val block: suspend FlowCollector<T>.() -> Unit
) : AbstractFlow<T>() {
    override suspend fun collectSafely(collector: FlowCollector<T>) {
        collector.block()
    }
}

flow 是一个流构建器函数,它用于创建一个冷流 SafeFlow 。"冷流"指的是每次应用终端运算符时(比如 collect)都会触发函数 block 的调用,因此该流在终端运算符被应用之前不会执行。该函数接受一个挂起的闭包参数 block,它需要传递给 FlowCollector 并执行。

SafeFlow 是一个内部类,它继承自抽象类 AbstractFlow<T>,并重写了 collectSafely 函数。在 collectSafely 函数中,它调用了传入的 block 参数,并将 FlowCollector 作为接收者传递给它。

通过这种方式,每当终端运算符被应用到冷流时,流的执行将从 collector.block() 开始执行,这样可以保证每次调用终端运算符时都重新执行一遍 block 中的代码。

在这段代码中还对流的上下文有一些说明。默认情况下,emit 操作符在调用时会检查流是否可取消,以保持流的上下文一致性。换句话说,每次调用 emit 时将调用 ensureActive 来检查流的激活状态。因此,如果在 block 中切换了执行上下文(如 withContext),在不同的上下文中调用 emit 可能会导致 IllegalStateException 异常。如果需要切换流的执行上下文,应使用 flowOn 操作符来改变执行上下文。

冷流与热流

在 Kotlin 中,冷流(Cold Flow)和热流(Hot Flow)是两种不同的流操作方式。

下面是 Kotlin Flow 冷流和热流的区别:

冷流(Cold Flow) 热流(Hot Flow)
数据共享 不共享数据 在多个收集者之间共享数据
数据生产时间 订阅时才开始产生数据 可以在订阅者订阅前就产生数据
取消订阅 取消订阅后不再接收数据 订阅取消订阅后可以继续接收数据
示例 flow { ... }asFlow() MutableSharedFlowSharedFlow

冷流是在每个订阅时才开始产生数据,并且每个订阅者都会独立接收到完整的数据序列。而热流在创建时或预先产生数据,在订阅时所有订阅者都可以接收到当前和之后的数据。取消订阅后,冷流不再接收数据,而热流可能继续接收数据。

在 Kotlin 中,冷流通常使用 flow { ... }(SafeFlow)或 asFlow() 来定义和表示,而热流可以使用 MutableSharedFlowSharedFlow 等类型来实现。

如上图所示,冷流 SafeFlow 用到了 FlowCollector ,而热流(MutableSharedFlowMutableStateFlow)本身就是一个 FlowCollectorSafeFlow 使用的 FlowCollectorSafeCollector ,在我们调用它的 collect 方法时,它会在父类 AbstractFlowcollect 方法中创建 SafeCollector。

而热流本身就是 FlowCollector ,这意味着不管有没有订阅者,我们都可以直接通过 emit 方法给它发送数据,emit 方法是 FlowCollector 接口声明的方法。而冷流只有在订阅的时候,才会在内部产生数据,再从内部发射出来给外部的收集者。Kotlin Flow 的冷流类似于 RxJava 中的 Observable ,而热流则类似于 RxJava 中的 BehaviorSubject

冷热流数据运算与通知方式对比

如图所示,冷流是在订阅者收集数据的时候开始运算新的数据,再返回给订阅者,每个订阅者收到的数据可能是不一样的,而热流则是由外部做好了数据运算后,保存到数据容器中,再通知给多个订阅者,每个订阅者在新数据产生时收到的数据是一样的。

AbstractFlow

Kotlin 复制代码
/**
 * Flow 的有状态实现的基类。
 * 它跟踪了所有需要保持上下文一致性所需的属性,并在违反任何属性时抛出 [IllegalStateException] 异常。
 */
@FlowPreview
public abstract class AbstractFlow<T> : Flow<T>, CancellableFlow<T> {

    @InternalCoroutinesApi
    public final override suspend fun collect(collector: FlowCollector<T>) {
        val safeCollector = SafeCollector(collector, coroutineContext)
        try {
            collectSafely(safeCollector)
        } finally {
            safeCollector.releaseIntercepted()
        }
    }

    /**
     * 接受给定的 [collector] 并将值[发射][FlowCollector.emit]到其中。
     *
     * 此方法的有效实现应满足以下约束:
     * 1) 当发射值时,不应更改协程上下文(例如使用 `withContext(Dispatchers.IO)`)。
     *    发射应在 [collect] 调用的上下文中发生。
     *    有关更多详细信息,请参阅顶级 [Flow] 文档。
     * 2) 应序列化对 [emit][FlowCollector.emit] 的调用,因为默认情况下 [FlowCollector] 实现不是线程安全的。
     *    可以使用 [channelFlow] 构建器代替 [flow] 来自动序列化发射。
     *
     * @throws IllegalStateException 如果违反任何不变量。
     */
    public abstract suspend fun collectSafely(collector: FlowCollector<T>)
}

SafeFlowAbstractFlow 的子类,collect 的具体实现就在 AbstractFlow 中,collect 方法只是简单创建了一个 SafeCollector ,并把它传给了 collectSafely() 方法,SafeFlow 的 collectSafely 做的只是把 FlowCollector 传给 flow 代码块。

AbstractFlow<T> 用于跟踪所有需要保持上下文一致性所需的属性,并在违反任何属性时抛出 IllegalStateException 异常。

AbstractFlow<T> 中,还有一个 collect 函数,它接受一个 FlowCollector<T> 对象,并调用 collectSafely 函数来进行值的发射。同时,它使用了 SafeCollector 对象,将传入的 collector 和协程上下文作为参数,以确保异常处理和资源释放的安全性。

注释中还指定了对 collectSafely 方法的要求:

  1. 在发射值时不应更改协程上下文,否则会抛出非法状态异常

  2. 应序列化对 emit 方法的调用,因为默认情况下 FlowCollector 的实现不是线程安全的。可以使用 channelFlow 构建器来自动序列化发射操作。

三、SingleProcessDataStore 数据读取流程

示例代码

Kotlin 复制代码
const val PERF_NAME = "my_perf"

// 数据迁移
private fun sharedPreferencesMigration(
    context: Context
): List<SharedPreferencesMigration<Preferences>> {
    return listOf(SharedPreferencesMigration(context, PERF_NAME))
}

val Context.dataStore by preferencesDataStore(
    name = "settings",
    produceMigrations = ::sharedPreferencesMigration
)

class MainActivity : AppCompatActivity() {

    companion object {
        private const val TAG = "MainActivity"
    }

    private lateinit var binding: ActivityMainBinding

    private val myDataKey = "my_data"
    private val preferencesKey1 = stringPreferencesKey(myDataKey)
    
    private val sharedPreferences by lazy {
        getSharedPreferences(PERF_NAME, MODE_PRIVATE)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        
        binding.btnCollect.setOnClickListener {
            // 收集数据
            collectData()
        }
        
        binding.btnUpdate.setOnClickListener {
            // 更新数据
            editData()
        }
    }

    private fun editData() {
        lifecycleScope.launch {
            dataStore.edit {
                it[preferencesKey1] = "111"
            }
            Log.d(TAG, "after edit---${getMyDataFomDataStore()}")
        }
    }

    // 获取 DataStore 的数据
    private fun collectData() {
        sharedPreferences.edit().putString(myDataKey, "value").apply()
        Log.d(TAG, "myData---1---before migration---${getMyDataFromSP()}")
        lifecycleScope.launch {
            // 获取数据
            dataStore.data.collect {
                val preferencesFromCollect = it[preferencesKey1]
                Log.d(
                    TAG, "myData---2---collect---$preferencesFromCollect" +
                            "---thread: ${Thread.currentThread().name}"
                )
            }
        }
        lifecycleScope.launch {
            Log.d(TAG, "myData---3---after migration---sp data: ${getMyDataFromSP()}")
        }
    }

    // 从 SP 中获取 Key 为 my_data 的 string 数据
    private fun getMyDataFromSP() = sharedPreferences.getString(myDataKey, null)

    // 从 DataStore 中获取 Key 为 my_data 的 string 数据
    private suspend fun getMyDataFomDataStore(): String? {
        return dataStore.data.map {
            it[preferencesKey1]
        }.firstOrNull()
    }
}

以上面这段代码为例,上面这段代码在 MainActivity 文件的顶部声明了一个 dataStore 全局变量和一个 sharedPreferencesMigration() 方法,sharedPreferencesMigration() 方法会返回一个 SharedPreferencesMigration 对象列表,也就是迁移操作,该迁移操作用于把 my_perf 中的数据迁移到 DataStore 中。

然后 MainActivity 中还声明了一个 collectData() 方法,该方法中会新创建一个专门的独立协程来收集 dataStore 的 data 数据流,myData---1 打印的值是 valuemyData---2 日志打印的值也是 value ,oldSPData 的值是空,因为迁移后 SharedPreferences 原来的文件就会被删除,所以 myData---3 打印的是 null。

然后在 editData 方法中,通过 edit 方法把 Key 为 myData 的偏好值改为了 111 ,所以 after edit 打印的值是 111。关于 edit 的实现后面后讲到。

以上面这段代码中的 data.collect 为例,下面来看下 PreferencesDataStore 的大致的数据读取流程。

流程概览

首先,当 MainActivity 收集 PreferencesDataStoredata 数据时,data 的具体实现在 SingleProcessDataStore 中,data 对应的 flow 代码块中,会提供(offer)一条 Read 消息给 SimpleActor ,SimpelActor 是 DataStore 中负责把读写消息放在子线程处理的一个对象。

然后 SimpleActor 会用创建 DataStore 时接收到的协程作用域来处理 ReadUpdate 消息,该作用域默认的分发器是 IO 协程分发器。对于 Read 消息,SimpleActor 会调用 SingleProcessDataStorehandleRead() 方法,让 dataStore 通过序列化器 Serializer 来读取存储偏好数据的 pb 文件的内容,读取到内容后,如果需要执行数据迁移,就会通过 SharedPreferencesMigration 把原有的 SharedPreferences 的 XML 文件中的内容写入 Preferences 对象中,最后把 Preferences 的数据封装到 Data 中,再设置给 data 下游的状态流 downstreamFlow ,当 downstreamFlow 接收到数据后,就会通知给 data ,然后再由 data 把数据通过恢复 MainActivity 的 collect 代码块的执行。

也就是 SingleProcessDataStore 中实际上是有两个数据流,一个数据流是对外的,提供给外部读取和修改数据的 data 数据流,该流是冷流 SafeFlow,另一个则是 downstreamFlow ,该流是热流 MutableStateFlow,是实际的负责数据读写的流,数据读写操作默认是在 IO 协程分发器中执行的。

更具体的时序图如下图所示。

为什么要用 pb 文件?PreferencesDataStore 的 pb 文件的结构是怎么样的?

Protocol Buffers(简称ProtoBuf)相对于 XML 具有更好的性能。它提供了更小的尺寸、更快的序列化和反序列化速度,以及更低的处理开销。

以下是 Protocol Buffers 相对于 XML 的性能优势:

  1. 尺寸更小:Protocol Buffers 使用二进制编码,相比于 XML 的纯文本格式,它生成的数据包是更小的。这意味着在网络传输和存储中,Protocol Buffers 可以减少带宽存储开销

  2. 序列化和反序列化速度更快:Protocol Buffers 的编解码过程相对较快,因为它使用了紧凑的二进制编码。这使得在大量数据的序列化和反序列化操作中,Protocol Buffers 通常比 XML 更高效

  3. 更好的性能特征:Protocol Buffers 提供了一种强类型的数据模型,支持数据结构的版本控制和向前兼容性。这使得在数据模式发生变化时,ProtoBuf 更具灵活性和容错性

  4. 更低的处理开销:Protocol Buffers 库生成高效的解析器和代码,这些代码针对所定义的消息类型进行优化。相比之下,XML 的解析库需要处理更多的元数据和标签解析,从而增加了额外的处理开销。

需要注意的是,对于简单的数据结构和小型数据,性能差异可能并不明显。但是,当面对大量复杂数据、高性能要求或带宽限制时,Protocol Buffers 的优势会更加明显。

虽然二进制编码在效率方面更高,但它对人类来说不够可读和可调试。相比之下,XML 是一种可读性更好的纯文本格式,易于理解和编辑。因此,在选择数据交换格式时,需要权衡可读性和效率之间的优先级,根据实际需求做出选择。

SharedPreferences 的 xml 文件与 DataStore 的 pb 文件

SharedPreferences:

用于 SharedPreferences 数据的 XML 文件在 /data/user/0/应用包名/shared_perfs 中,文件的名字就是我们创建 SharedPreferences 时所传的 name 。

XML 文件的内容如下,在 map 标签中,包含了根据类型区分的如 string 等标签。

DataStore:

用于存储 DataStore 数据的 pb 文件在 /data/user/0/应用包名/files/datastore 目录下,文件名则是设定的名称 + .preferences_pb ,比如 settings.preferences_pb

文件的内容如下,以 key * value 的形式保存。

SingleProcessDataStore#data

Kotlin 复制代码
/**
 * DataStore 的单进程实现
 */
internal class SingleProcessDataStore<T>(
    private val produceFile: () -> File,
    private val serializer: Serializer<T>,
    initTasksList: List<suspend (api: InitializerApi<T>) -> Unit> = emptyList(),
    private val corruptionHandler: CorruptionHandler<T> = NoOpCorruptionHandler<T>(),
    private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
) : DataStore<T> {

    // 用于收集和获取 preference 数据的数据流,当被收集(collect)时就会执行代码块
    override val data: Flow<T> = flow {
        // 下游数据流状态
        val currentDownStreamFlowState = downstreamFlow.value

        if (currentDownStreamFlowState !is Data) {
            // 如果当前下游流的状态不是 Data,说明还没读取到数据,需要发送 Read 消息给 Channel,
            // 触发新的读取请求
            actor.offer(Message.Read(currentDownStreamFlowState))
        }

        // 使用 dropWhile() 方法丢弃满足特定条件的元素,并发射数据给冷流收集者
        emitAll(
            downstreamFlow.dropWhile {
                if (currentDownStreamFlowState is Data<T> ||
                    currentDownStreamFlowState is Final<T>
                ) {
                    // 不需要丢弃任何 Data 或 Final 值
                    false
                } else {
                    // 需要丢弃最后一个已看到的状态,因为它要么是异常,要么尚未初始化。
                    // 由于我们向 actor 发送了一条消息,我们将会看到一个新值。
                    it === currentDownStreamFlowState
                }
            }.map {
                when (it) {
                    // 抛出读取异常
                    is ReadException<T> -> throw it.readException
                  
                    // 抛出最终异常
                    is Final<T> -> throw it.finalException
                  
                    // 返回数据值
                    is Data<T> -> it.value
                  
                    // 抛出未初始化异常
                    is UnInitialized -> error(
                        "This is a bug in DataStore. Please file a bug at: " +
                            "https://issuetracker.google.com/issues/new?" +
                            "component=907884&template=1466542"
                    )
                }
            }
        )
    }
}

在上面这段代码中,当 DataStore 的 data 被收集(collect)时,SingleProcessDataStore 中的 flow 代码块就会开始执行。

SingleProcessDataStore 是 DataStore 的单进程实现,不保证在多进程环境下的安全性。

该类实现了 DataStore<T> 接口,并重写了其中的方法和属性。它接受以下参数:

  • produceFile:用于返回要操作的文件的函数

  • serializer:用于序列化和反序列化数据的序列化器

  • initTasksList:初始化任务列表,具体类型是一个挂起函数列表,执行这些任务会在发布任何数据到数据流之前完成,也会在 updateData 方法中的读写操作之前执行。如果其中任何任务失败,将在下次收集数据或调用 updateData 时重新运行这些任务。初始化任务不应该等待数据的结果,否则可能导致死锁

  • corruptionHandler:序列化失败处理器,用于处理读取数据时遇到的损坏异常

  • scope:用于执行 IO 操作和转换函数的协程作用域

SingleProcessDataStore 实现了数据的读取和发送,通过 data 属性将数据作为数据流进行访问,并处理了不同的状态和异常情况。根据实际需求,可以使用该类来创建单进程的 DataStore 实例。

其中,data 属性是一个 Flow(流),用于将数据作为数据流进行访问。当有人收集(collect)data 的数据时,机会调用 flow 构建器函数,该函数的具体实现如下:

  • 首先获取当前下游流的状态

  • 如果当前下游流的状态不是 Data,说明尚未读取到数据,需要通过 actor.offer 触发新的读取请求。

  • 通过 emitAll() 发送数据给下游流,根据下游流的不同状态进行处理:

    • 如果当前下游流的状态是 ReadException,说明上次读取数据时发生了异常,抛出读取异常

    • 如果当前下游流的状态是 Final,说明作用域已被取消,数据存储不可用,直接传播该异常

    • 如果当前下游流的状态是 Data,表示已经有数据,将其发送给下游流

    • 如果当前下游流的状态是 UnInitialized,说明这是 DataStore 的一个 bug

emitAll 、 dropWhile 与 map

Kotlin 复制代码
public suspend fun <T> FlowCollector<T>.emitAll(flow: Flow<T>) {
    ensureActive()
    flow.collect(this)
}

emitAll 方法用于直接把收集到的值发射给收集者,是 flow.collect { value -> emit(value) } 的简写形式。在前面示例代码的 flow 代码块中,收集者就是 SafeCollector 。

Kotlin 复制代码
public fun <T> Flow<T>.dropWhile(predicate: suspend (T) -> Boolean): Flow<T> = flow {
    var matched = false
    collect { value ->
        if (matched) {
            emit(value)
        } else if (!predicate(value)) {
            matched = true
            emit(value)
        }
    }
}

dropWhile(过滤直到),用于从流中删除满足条件的元素,直到第一个不满足条件的元素。dropWhile 方法会创建一个新的冷流,新的 flow 代码块中会 predicate 函数的值判断从调用者(Flow)收集到的数据是否符合条件,不符合条件时,就把该值发射出去,下一次 downstreamFlow 有新值时,如果已经匹配过了,就会直接发射收到的数据。

Kotlin 复制代码
public inline fun <T, R> Flow<T>.map(crossinline transform: suspend (value: T) -> R): Flow<R> = transform { value ->
   return@transform emit(transform(value))
}

map 扩展函数用于对流进行变换操作,该函数会返回一个 flow,其中包含将给定的 [transform] 函数应用于原始 flow 的每个值后的结果。

SingleProcessDataStore#actor

Kotlin 复制代码
/**
 * 当前 DataStore 的状态
 */
private sealed class State<T>

internal class SingleProcessDataStore<T>(
    private val produceFile: () -> File,
    private val serializer: Serializer<T>,
    initTasksList: List<suspend (api: InitializerApi<T>) -> Unit> = emptyList(),
    private val corruptionHandler: CorruptionHandler<T> = NoOpCorruptionHandler<T>(),
    private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
) : DataStore<T> {
  
    @Suppress("UNCHECKED_CAST")
    private val downstreamFlow = MutableStateFlow(UnInitialized as State<T>)
    
    private val actor = SimpleActor<Message<T>>(
        scope = scope,
      
        // 协程作用域中的 Job 完成回调
        onComplete = {
            it?.let {
                downstreamFlow.value = Final(it)
            }

            synchronized(activeFilesLock) {
                // 从活跃的文件中移除
                activeFiles.remove(file.absolutePath)
            }
        },
      
        // 未分发元素回调( Job 完成时可能被调用)
        onUndeliveredElement = { msg, ex ->
            if (msg is Message.Update) {
                // 把异常通知给数据更新者
                msg.ack.completeExceptionally(
                    ex ?: CancellationException(
                        "DataStore scope was cancelled before updateData could complete"
                    )
                )
            }
        }
        
    // 消费消息回调
    ) { msg ->
        when (msg) {
            is Message.Read -> {
                // 处理读操作
                handleRead(msg)
            }
            is Message.Update -> {
                // 处理写操作
                handleUpdate(msg)
            }
        }
    }
}

actor 是一个用于处理消息的 SimpleActor 对象。它接收 Message<T> 类型的消息,并根据消息的类型执行相应的操作。

  • scope 是协程的作用域。

  • onComplete 是一个回调函数,在 actor 执行完所有消息后被调用。它将最终的结果发布到下游的数据流 downstreamFlow 中,并从 activeFiles 中移除相应的文件。

  • onUndeliveredElement 是一个回调函数,当无法投递消息时被调用。如果消息是更新消息 Message.Update,则将 ack 设置为异常状态,表示数据存储的作用域在 updateData 完成之前被取消。

  • msg 是接收到的消息,根据消息的类型执行相应的操作。如果是读取消息 Message.Read,则调用 handleRead 方法;如果是更新消息 Message.Update,则调用 handleUpdate 方法。

downstreamFlow 是一个可变的状态流。初始状态是未初始化 UnInitialized,类型为 State<T>,并将其强制转换为 MutableStateFlow,即可变状态流,State 用于表示 DataStore 当前的状态。

SingleActor

Kotlin 复制代码
internal class SimpleActor<T>(
    /**
     * 用于消费消息的作用域。
     */
    private val scope: CoroutineScope,
  
    /**
     * 当作用域被取消时调用的函数。不应抛出异常。
     */
    onComplete: (Throwable?) -> Unit,
  
    /**
     * 当作用域被取消时,对每个元素调用的函数。不应抛出异常。
     */
    onUndeliveredElement: (T, Throwable?) -> Unit,
  
    /**
     * 对每条消息调用一次的函数。
     *
     * 除非作用域被取消,否则不应抛出异常(除非是CancellationException)。
     */
    private val consumeMessage: suspend (T) -> Unit
) {
  
    // 消息队列
    private val messageQueue = Channel<T>(capacity = UNLIMITED)

    /**
     * 剩余待处理消息的数量
     */
    private val remainingMessages = AtomicInteger(0)

    init {
        // 如果作用域没有任务,它不会被取消,因此不需要注册回调。
        scope.coroutineContext[Job]?.invokeOnCompletion { ex ->
            onComplete(ex)
            
            messageQueue.close(ex)

            while (true) {
                // 把无法分发的消息回调给 StateFlow
                messageQueue.tryReceive().getOrNull()?.let { msg ->
                    onUndeliveredElement(msg, ex)
                } ?: break
            }
        }
    }

    /**
     * 将消息发送到消息队列,由[scope]中的[consumeMessage]处理。
     *
     * 如果[offer]成功完成,消息将被[consumeMessage]或[onUndeliveredElement]处理。
     * 如果[offer]抛出异常,消息可能会被处理,也可能不会。
     */
    fun offer(msg: T) {
        /**
         * 可能的状态:
         * 1)remainingMessages = 0
         *    所有消息都已被消费,因此没有活动的消费者。
         * 2)remainingMessages > 0,没有活动的消费者
         *    其中一个发送者负责触发消费者。
         * 3)remainingMessages > 0,有活动的消费者
         *    消费者将继续消费,直到remainingMessages为0。
         * 4)messageQueue已关闭,还有待消费的消息
         *    尝试offer消息将失败,onComplete()将使用onUndelivered消费剩余消息。
         *    消费者已经完成,因为close()由onComplete()调用。
         * 5)messageQueue已关闭,没有待消费的消息
         *    尝试offer消息将失败。
         */

        // 由于通道容量是无限的,所以不应返回false
        check(
            messageQueue.trySend(msg)
                .onClosed { throw it ?: ClosedSendChannelException("Channel was closed normally") }
                .isSuccess
        )

        // 如果剩余消息的数量为0,则没有活动的消费者,因为一旦剩余消息达到0,消费者就会停止消费。
        // 我们必须启动一个新的消费者。
        if (remainingMessages.getAndIncrement() == 0) {
            scope.launch {
                // 除非还有剩余消息,否则不应该启动新的消费者
                check(remainingMessages.get() > 0)

                do {
                    // 只有当我们仍然活动时,才希望尝试消费新的消息。
                    // 如果ensureActive抛出异常,则作用域不再活动,因此剩余消息无关紧要。
                    scope.ensureActive()

                    consumeMessage(messageQueue.receive())
                } while (remainingMessages.decrementAndGet() != 0)
            }
        }
    }
}

SimpleActor 中维护了一个消息队列 messageQueueoffer 方法会开启一个不断读取消息的循环 。

  • 构造函数:SimpleActor 的构造函数接受泛型参数 T,代表消息的类型,并初始化了以下成员变量:

    • scope:消费消息的协程作用域

    • onComplete:当 scope 被取消时调用的函数,它接受一个 Throwable 类型的参数,表示可能的异常。

    • onUndeliveredElement:当 scope 被取消时,对每个元素调用的函数,它接受两个参数:一个表示消息的元素和一个 Throwable 类型的参数,表示可能的异常。

    • consumeMessage:对每个消息调用的函数,它接受一个类型为 T 的参数,并且在 scope 中被挂起执行。

  • 成员变量:

    • messageQueue:一个 Channel 对象,用于存储消息的队列

    • remainingMessages:一个用于记录剩余待处理消息的计数的对象

  • 初始化代码块:在初始化代码块中,通过 scope.coroutineContext[Job]?.invokeOnCompletion { ... } 注册了一个回调函数,当 scope 被取消时被调用。在回调函数中:

    • 调用 onComplete(ex),将可能的异常 ex 传递给 onComplete 函数。

    • 调用 messageQueue.close(ex),关闭消息队列,传递可能的异常 ex

    • 使用 messageQueue.tryReceive().getOrNull()?.let { msg -> onUndeliveredElement(msg, ex) } 循环处理剩余的消息,将每个消息 msg 和可能的异常 ex 传递给 onUndeliveredElement 函数。

  • offer 函数:这个函数将消息发送到消息队列,由 consumeMessagescope 中进行处理。

    • 首先使用 messageQueue.trySend(msg) 尝试将消息发送到消息队列,并且如果通道已关闭,则抛出异常。

    • 如果剩余消息的数量为 0,表示没有活跃的消费者,那么需要启动一个新的消费者。通过 remainingMessages.getAndIncrement() == 0 条件判断。

    • 在新的消费者中,使用 messageQueue.receive() 接收消息,并通过 consumeMessage(messageQueue.receive()) 将消息传递给 consumeMessage 函数进行处理。

    • 消费完消息后,通过 remainingMessages.decrementAndGet() != 0 判断剩余消息的数量,如果是 0,则停止消费。如果不是 0,则继续循环消费下一个消息

SingleProcessDataStore#handleRead

Kotlin 复制代码
internal class SingleProcessDataStore<T>(
    private val produceFile: () -> File,
    private val serializer: Serializer<T>,
    initTasksList: List<suspend (api: InitializerApi<T>) -> Unit> = emptyList(),
    private val corruptionHandler: CorruptionHandler<T> = NoOpCorruptionHandler<T>(),
    private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
) : DataStore<T> {
 
    private suspend fun handleRead(read: Message.Read<T>) {
        when (val currentState = downstreamFlow.value) {
            is Data -> {
                // 已经有了数据,所以只需返回...
            }
            is ReadException -> {
                if (currentState === read.lastState) {
                    readAndInitOrPropagateFailure()
                }

                // 别人抢先一步但也失败了。收集器已经被通知,所以我们不需要做任何事情
            }
            UnInitialized -> {
                // 读取数据
                readAndInitOrPropagateFailure()
            }
            is Final -> error("Can't read in final state.") // 不会发生
        }
    }
}

handleRead 方法用于处理读取操作。在方法体中,根据当前的状态 currentState 进行不同的处理:

  • 如果当前状态是 Data,表示已经存在数据,直接返回

  • 如果当前状态是 ReadException,意味着先前的读取操作出现异常。如果当前状态等于 read.lastState,则执行 readAndInitOrPropagateFailure 函数来重新读取数据并进行初始化或传播失败

  • 如果当前状态是 UnInitialized,表示数据未初始化,则执行 readAndInitOrPropagateFailure 函数来进行读取和初始化或传播失败

  • 如果当前状态是 Final,发生了错误,因为在最终状态下不应该执行读取操作

SingleProcessDataStore#readAndInitOrPropagateFailure

Kotlin 复制代码
internal class SingleProcessDataStore<T>(
    private val produceFile: () -> File,
    private val serializer: Serializer<T>,
    initTasksList: List<suspend (api: InitializerApi<T>) -> Unit> = emptyList(),
    private val corruptionHandler: CorruptionHandler<T> = NoOpCorruptionHandler<T>(),
    private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
) : DataStore<T> {
 
    private suspend fun readAndInitOrPropagateFailure() {
        try {
            readAndInit()
        } catch (throwable: Throwable) {
            downstreamFlow.value = ReadException(throwable)
        }
    }

    private suspend fun readAndInit() {
        // This should only be called if we don't already have cached data.
        check(downstreamFlow.value == UnInitialized || downstreamFlow.value is ReadException)

        val updateLock = Mutex()
        var initData = readDataOrHandleCorruption()

        var initializationComplete: Boolean = false

        val api = object : InitializerApi<T> {
            override suspend fun updateData(transform: suspend (t: T) -> T): T {
                return updateLock.withLock() {
                    if (initializationComplete) {
                        throw IllegalStateException(
                            "InitializerApi.updateData should not be " +
                                "called after initialization is complete."
                        )
                    }
                    
                    // 执行迁移(DataMigrationInitializer#runMigrations)
                    val newData = transform(initData)
                    if (newData != initData) {
                        // 写入数据
                        writeData(newData)
                        initData = newData
                    }

                    initData
                }
            }
        }
    
        // 设置迁移初始化 API
        initTasks?.forEach { it(api) }
        initTasks = null // Init tasks have run successfully, we don't need them anymore.
        updateLock.withLock {
            initializationComplete = true
        }

        // 把数据传给下游数据流
        downstreamFlow.value = Data(initData, initData.hashCode())
    }

}
  1. readAndInitOrPropagateFailure函数用于读取和初始化数据,它首先尝试调用readAndInit函数来读取和初始化数据,如果出现异常,则将异常传播给下游流

  2. readAndInit函数用于读取和初始化数据。它首先检查当前是否有缓存数据,如果没有或者缓存数据存在读取异常,则继续执行以下操作:

    • 创建一个互斥锁 updateLock,用于保证线程安全。

    • 通过调用readDataOrHandleCorruption函数来读取数据或处理数据损坏。得到初始数据initData

    • 定义一个布尔变量initializationComplete用于表示初始化是否完成。

    • 创建一个InitializerApi<T>对象供初始化任务使用,该对象提供了更新数据的方法updateData,在控制并发访问和更新数据的过程中加锁处理。

    • 依次执行初始化任务列表initTasks,并传入api对象供任务使用。

    • 初始化任务完成后,将initTasks设置为null,表示不再需要初始化任务。

    • 最后通过互斥锁更新initializationComplete状态,并将数据和哈希码封装成Data对象发送给下游流。

SingleProcessDataStore#readDataOrHandleCorruption

Kotlin 复制代码
internal class SingleProcessDataStore<T>(
    private val produceFile: () -> File,
    private val serializer: Serializer<T>,
    initTasksList: List<suspend (api: InitializerApi<T>) -> Unit> = emptyList(),
    private val corruptionHandler: CorruptionHandler<T> = NoOpCorruptionHandler<T>(),
    private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
) : DataStore<T> {
    
    private suspend fun readDataOrHandleCorruption(): T {
        try {
            return readData()
        } catch (ex: CorruptionException) {

            val newData: T = corruptionHandler.handleCorruption(ex)

            try {
                writeData(newData)
            } catch (writeEx: IOException) {
                // If we fail to write the handled data, add the new exception as a suppressed
                // exception.
                ex.addSuppressed(writeEx)
                throw ex
            }

            // If we reach this point, we've successfully replaced the data on disk with newData.
            return newData
        }
    }
    
    private suspend fun readData(): T {
        try {
            FileInputStream(file).use { stream ->
                return serializer.readFrom(stream)
            }
        } catch (ex: FileNotFoundException) {
            if (file.exists()) {
                throw ex
            }
            return serializer.defaultValue
        }
    }
}
  1. readDataOrHandleCorruption() 函数用于读取数据或处理数据损坏。它首先尝试调用 readData() 函数来读取数据,如果读取过程中出现 CorruptionException 异常,则调用 corruptionHandler 接口的 handleCorruption() 方法处理数据损坏,并将处理后的新数据 newData 写入文件。如果在写入数据时发生 I/O 异常,则将该异常添加为 CorruptionException 的被抑制异常,并重新抛出。最终,函数返回读取的数据或处理后的新数据。

  2. readData() 函数用于从文件中读取数据。它尝试以文件输入流的形式打开文件,然后使用 serializer 接口的 readFrom() 方法从流中反序列化并返回数据。如果文件不存在,则判断文件存在异常是否被抛出,如果抛出则重新抛出该异常;否则返回 serializer 接口的默认值。

总体来说,这段代码实现了一个数据存储类 SingleProcessDataStore,通过读取文件、处理数据损坏等操作来提供数据的读取和更新功能。其中的异常处理机制确保了在数据损坏或写入失败时的稳定性。

SingleProcessDataStore#writeData

Kotlin 复制代码
internal class SingleProcessDataStore<T>(
    private val produceFile: () -> File,
    private val serializer: Serializer<T>,
    initTasksList: List<suspend (api: InitializerApi<T>) -> Unit> = emptyList(),
    private val corruptionHandler: CorruptionHandler<T> = NoOpCorruptionHandler<T>(),
    private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
) : DataStore<T> {

    internal suspend fun writeData(newData: T) {
        file.createParentDirectories()

        val scratchFile = File(file.absolutePath + SCRATCH_SUFFIX)
        try {
            FileOutputStream(scratchFile).use { stream ->
                serializer.writeTo(newData, UncloseableOutputStream(stream))
                stream.fd.sync()
                // TODO(b/151635324): fsync the directory, otherwise a badly timed crash could
                //  result in reverting to a previous state.
            }

            if (!scratchFile.renameTo(file)) {
                throw IOException(
                    "Unable to rename $scratchFile." +
                        "This likely means that there are multiple instances of DataStore " +
                        "for this file. Ensure that you are only creating a single instance of " +
                        "datastore for this file."
                )
            }
        } catch (ex: IOException) {
            if (scratchFile.exists()) {
                scratchFile.delete() // Swallow failure to delete
            }
            throw ex
        }
    }
}

这段代码是一个单进程数据存储类 SingleProcessDataStore,实现了 DataStore<T> 接口。它用于将数据以文件的形式存储到磁盘上。

构造函数接收一些参数:

  • produceFile: 一个函数,用于生成保存数据的文件

  • serializer: 数据的序列化器,用于将数据对象转换为字节流进行存储和读取。

  • initTasksList: 初始化任务列表,它是一个挂起函数的列表,在初始化数据存储时会按顺序执行。

  • corruptionHandler: 数据损坏处理器,用于在遇到存储文件损坏时进行处理,默认为 NoOpCorruptionHandler,即不进行任何处理

  • scope: 协程作用域,用于协程管理。

writeData 函数用于将新的数据写入到文件中。首先创建父目录确保文件路径存在,然后创建一个临时文件(使用 ".tmp" 后缀),将新的数据通过序列化器写入到临时文件中。写入完成后,将临时文件重命名为正式的文件名称,完成数据的替换更新。

如果重命名失败,则抛出异常,可能是因为有多个实例同时操作同一个文件。在异常处理中,如果临时文件存在,则删除临时文件,并继续抛出异常。

整体来说,该代码实现了将数据以文件形式存储并进行更新的功能。

四、PreferencesKeys

PreferencesKeys

Kotlin 复制代码
@file:JvmName("PreferencesKeys")

package androidx.datastore.preferences.core

@JvmName("intKey")
public fun intPreferencesKey(name: String): Preferences.Key<Int> = Preferences.Key(name)

@JvmName("doubleKey")
public fun doublePreferencesKey(name: String): Preferences.Key<Double> = Preferences.Key(name)

@JvmName("stringKey")
public fun stringPreferencesKey(name: String): Preferences.Key<String> = Preferences.Key(name)

@JvmName("booleanKey")
public fun booleanPreferencesKey(name: String): Preferences.Key<Boolean> = Preferences.Key(name)

@JvmName("floatKey")
public fun floatPreferencesKey(name: String): Preferences.Key<Float> = Preferences.Key(name)

@JvmName("longKey")
public fun longPreferencesKey(name: String): Preferences.Key<Long> = Preferences.Key(name)

@JvmName("stringSetKey")
public fun stringSetPreferencesKey(name: String): Preferences.Key<Set<String>> =
    Preferences.Key(name)

在 PreferencesKeys 文件中,定义了获取 stringPreferencesKey 等多个 Preference Key 的方法,比如 int、double、string、boolean、float、long 和 stringSet 等。这些方法的返回值都是 Preferences.Key 。

Preferences.Key

Kotlin 复制代码
/**
 * Preferences和MutablePreferences类似于以Preferences.Key类作为键的通用Map和MutableMap。
 * 这些类旨在与DataStore一起使用。使用[PreferenceDataStoreFactory.create]构造一个DataStore<Preferences>实例。
 */
public abstract class Preferences internal constructor() {
    /**
     * 存储在Preferences中的值的键。类型T是与该键关联的值的类型。
     *
     * T必须是以下类型之一:Boolean、Int、Long、Float、String、Set<String>。
     *
     * 使用以下方法为您的数据类型构造键:[booleanPreferencesKey]、[intPreferencesKey]、
     * [longPreferencesKey]、[floatPreferencesKey]、[stringPreferencesKey]、[stringSetPreferencesKey]
     */
    public class Key<T>
    internal constructor(public val name: String) {
        /**
         * 中缀函数,用于创建Preferences.Pair。
         * 这用于支持[preferencesOf]和[MutablePreferences.putAll]方法。
         * @param value 是该偏好设置键应指向的值。
         */
        public infix fun to(value: T): Preferences.Pair<T> = Preferences.Pair(this, value)

        // ...
    }

    // ...
    
    /**
     * 获取一个可变的Preferences副本,其中包含此Preferences中的所有偏好设置。
     * 这可用于更新偏好设置,而无需从头构建新的Preferences对象,只需在[DataStore.updateData]中使用它即可。
     *
     * 这类似于[Map.toMutableMap]方法。
     *
     * @return 包含此Preferences中所有偏好设置的MutablePreferences
     */
    public fun toMutablePreferences(): MutablePreferences {
        return MutablePreferences(asMap().toMutableMap(), startFrozen = false)
    }

    /**
     * 获取一个只读副本,其中包含此Preferences中的所有偏好设置。
     *
     * 这类似于[Map.toMap]方法。
     *
     * @return 此Preferences的副本
     */
    public fun toPreferences(): Preferences {
        return MutablePreferences(asMap().toMutableMap(), startFrozen = true)
    }
}

Preferences.Key 类中,声明了一个 to 方法,用于创建存到 Preferences 中的键值对 Pair ,比如下面这样。

Kotlin 复制代码
dataStore.edit {
  it.putAll(
    key to "100"
  )
}

五、SingleProcessDataStore 数据更新流程

概览

当我们调用 PreferenceDataStoreedit 方法时,该方法的具体实现在 SingleProcessDataStoreupdateData 方法中,在 updateData 方法中,首先会给 SimpleActor 发一条 Update 消息,并且会把一个 CompletableDeferred 对象放在该消息中,该对象用于等待 Update 消息执行完成。

SimpleActor 接收到 Update 消息后,就会调用 SingleProcessDataStore 的 handleUpdate 方法开始更新数据,如果当前的下游流的状态是未初始化的话,则会先读取 pb 文件的数据,如果需要执行迁移操作的话,还会执行迁移操作。然后会执行 edit 代码块(transform)更新 Preferences 数据,再把数据写入到 pb 文件中,最后更新下游流的状态值。

edit

Kotlin 复制代码
/**
 * @param transform 接受包含 DataStore 中所有首选项的 MutablePreferences 的匿名函数。对此 MutablePreferences 对象的更改将在 transform 完成后持久化。
 * @throws IOException 写入数据到磁盘时遇到异常时抛出
 * @throws Exception 转换块抛出异常时抛出
 */

public suspend fun DataStore<Preferences>.edit(
    transform: suspend (MutablePreferences) -> Unit
): Preferences {
    return this.updateData {
        // 在 PreferencesDataStore.updateData() 中冻结 MutablePreferences,因此返回它是安全的
        it.toMutablePreferences().apply { transform(this) }
    }
}

edit 方法用于以原子方式在事务中编辑 DataStore 中的值,进行读取-修改-写入操作。所有操作都是串行化的。当数据被持久化到磁盘后(之后 [DataStore.data] 将反映更新),协程才会完成。如果转换过程或写入磁盘失败,事务将被中止并抛出异常。

edit 方法中会调用 updateData 方法,并在 updateData 代码块中把获取到的 Preferences 转换为可变的偏好数据 MutablePreferences ,然后再把 MutablePreferences 传给 transform 函数。

transform 参数:transform 接受包含 DataStore 中所有首选项的 MutablePreferences 的匿名函数。对此 MutablePreferences 对象的更改将在 transform 完成后持久化。

注意:在使用 edit 方法时要捕获异常,因为该方法有可能抛出 IO 异常,或者 transform 代码块本身就有异常。

注意:在 [transform] 中更改的值,在转换完成之前不会更新到 DataStore 中。

注意:不要保存对传递给 transformMutablePreferences 的引用。在 [transform] 返回后对其进行修改不会更改 DataStore 中的数据。

使用示例:

Kotlin 复制代码
val COUNTER_KEY = intPreferencesKey("my_counter")
dataStore.edit { prefs ->
  prefs[COUNTER_KEY] = (prefs[COUNTER_KEY] ?: 0) + 1
}

updateData

Kotlin 复制代码
internal class PreferenceDataStore(private val delegate: DataStore<Preferences>) :
    DataStore<Preferences> by delegate {
    
    override suspend fun updateData(transform: suspend (t: Preferences) -> Preferences):
            Preferences {
        return delegate.updateData {
            val transformed = transform(it)
            // 冻结 Preferences ,因为如果用户将值从 DataStore 中取出并进行变异,这可能会造成问题。
            // 这是安全的强制转换,因为 MutablePreferences 是 Preferences 的唯一实现。
            (transformed as MutablePreferences).freeze()
            transformed
        }
    }
}

PreferencesDataStore 的 updateData 方法的具体实现在 SingleProcessDataStore 中,在调用 SingleProcessDataStore 的 updateData 代码时,会把 transform 后的 MutablePreferences 冻结。

MutablePreferences#freeze

Kotlin 复制代码
/**
 * [Preferences] 的可变版本. 允许用不同的键值对创建 Preferences
 */
public class MutablePreferences internal constructor(
    internal val preferencesMap: MutableMap<Key<*>, Any> = mutableMapOf(),
    startFrozen: Boolean = true
) : Preferences() {
  
    /**
     * 如果设置为冻结状态,将会在进行修改操作时抛出异常。
     */
    private val frozen = AtomicBoolean(startFrozen)

    internal fun checkNotFrozen() {
        check(!frozen.get()) { "一旦返回到 DataStore,不要对 Preferences 进行修改。" }
    }

    /**
     * 导致后续的修改操作引发异常
     */
    internal fun freeze() {
        frozen.set(true)
    }
    
    public operator fun <T> set(key: Key<T>, value: T) {
        setUnchecked(key, value)
    }

    /**
     * 私有的设置函数。键和值的类型必须相同。
     */
    internal fun setUnchecked(key: Key<*>, value: Any?) {
        checkNotFrozen()

        when (value) {
            null -> remove(key)
            // 复制集合,以免更改输入的内容导致 Preferences 发生变化。
            // 包装在 unmodifiableSet 中,使得返回的实例不可更改。
            is Set<*> -> preferencesMap[key] = Collections.unmodifiableSet(value.toSet())
            else -> preferencesMap[key] = value
        }
    }
  
}

freeze() 方法用于修改 MutablePreferences 中的 frozen 状态值,如果 MutablePreferences 被冻结了,就无法被修改,修改时会抛出异常。

SingleProcessDataStore#updateData

Kotlin 复制代码
internal class SingleProcessDataStore<T>(
    private val produceFile: () -> File,
    private val serializer: Serializer<T>,
    initTasksList: List<suspend (api: InitializerApi<T>) -> Unit> = emptyList(),
    private val corruptionHandler: CorruptionHandler<T> = NoOpCorruptionHandler<T>(),
    private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
) : DataStore<T> {

    override suspend fun updateData(transform: suspend (t: T) -> T): T {
        /**
         * 这里的状态与读取时的状态相同。此外,我们发送一个确认消息,actor 必须对其做出响应(即使被取消)。
         */
        val ack = CompletableDeferred<T>()
        val currentDownStreamFlowState = downstreamFlow.value

        val updateMsg =
            Message.Update(transform, ack, currentDownStreamFlowState, coroutineContext)

        actor.offer(updateMsg)

        return ack.await()
    }
    
}

SingleProcessDataStore 的 updateData 方法中,会创建一个 CompletableDeferred 对象,CompletableDeferred 实现了 Deferred 接口,Deferred 接口定义了 await() 方法,Deferred 接口实现了 Job 接口,也就是 CompletableDeferred 是协程框架中的一个可以等待完成的任务

updateData() 方法中,创建了 CompletableDeferred 后,就会发送一条 Update 消息给 SimpleActor ,然后等待 CompletableDeferred 的执行结果。

SimpleActor 收到 Update 消息后,就会让 SingleProcessDataStore 执行 handleUpdate 方法。

SingleProcessDataStore#handleUpdate

Kotlin 复制代码
internal class SingleProcessDataStore<T>(
    private val produceFile: () -> File,
    private val serializer: Serializer<T>,
    initTasksList: List<suspend (api: InitializerApi<T>) -> Unit> = emptyList(),
    private val corruptionHandler: CorruptionHandler<T> = NoOpCorruptionHandler<T>(),
    private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
) : DataStore<T> {

    private val actor = SimpleActor<Message<T>>(
        scope = scope,
        // ...
    ) { msg ->
        when (msg) {
            is Message.Read -> {
                handleRead(msg)
            }
            is Message.Update -> {
                handleUpdate(msg)
            }
        }
    }

    /**
     * 处理更新消息
     */
    private suspend fun handleUpdate(update: Message.Update<T>) {
        // 所有分支操作都必须正常或异常地完成 ack
        // 不能抛出异常,只能将异常传播给 ack
        update.ack.completeWith(
            runCatching {
                when (val currentState = downstreamFlow.value) {
                    is Data -> {
                        // 已经初始化,只需要执行更新操作
                        transformAndWrite(update.transform, update.callerContext)
                    }
                    is ReadException, is UnInitialized -> {
                        if (currentState === update.lastState) {
                            // 需要重新尝试读取
                            readAndInitOrPropagateAndThrowFailure()

                            // 成功读取后执行更新操作
                            transformAndWrite(update.transform, update.callerContext)
                        } else {
                            // 别人已经先于我们进行了读取,但同样失败。现在我们只需要向等待 ack 的写入者发出信号。
                            // 这里的类型转换是安全的,因为如果状态已经改变,我们不可能处于未初始化状态。
                            throw (currentState as ReadException).readException
                        }
                    }

                    is Final -> throw currentState.finalException // 不会发生的情况
                }
            }
        )
    }
}

在 handleUpdate 方法中,会判断当前下游流的状态是否为未初始化,如果是未初始化的话,就要先读取数据和执行迁移操作,如果已初始化,就通过 transformAndWrite 方法修改 Preferences 并把新的数据写入文件。

SingleProcessDataStore#transformAndWrite

Kotlin 复制代码
internal class SingleProcessDataStore<T>(
    private val produceFile: () -> File,
    private val serializer: Serializer<T>,
    initTasksList: List<suspend (api: InitializerApi<T>) -> Unit> = emptyList(),
    private val corruptionHandler: CorruptionHandler<T> = NoOpCorruptionHandler<T>(),
    private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
) : DataStore<T> {
    
    // downstreamFlow.value 必须在调用此函数之前成功设置为 data
    private suspend fun transformAndWrite(
        transform: suspend (t: T) -> T,
        callerContext: CoroutineContext
    ): T {
        // 因为在这个时候值必须已经被设置,所以 value 不会是 null 或异常,这次类型转换是安全的。
        val curDataAndHash = downstreamFlow.value as Data<T>
        curDataAndHash.checkHashCode()

        val curData = curDataAndHash.value
        val newData = withContext(callerContext) { transform(curData) }

        // 检查 curData 是否变化...
        curDataAndHash.checkHashCode()

        return if (curData == newData) {
            curData
        } else {
            writeData(newData)
            downstreamFlow.value = Data(newData, newData.hashCode())
            newData
        }
    }
    
    private val SCRATCH_SUFFIX = ".tmp"
  
    /**
     * 仅用于阻止创建合成访问函数。请勿在此类的外部调用该函数
     */
    internal suspend fun writeData(newData: T) {
        file.createParentDirectories()

        val scratchFile = File(file.absolutePath + SCRATCH_SUFFIX)
        try {
            FileOutputStream(scratchFile).use { stream ->
                serializer.writeTo(newData, UncloseableOutputStream(stream))
                stream.fd.sync()
            }

            // 重命名
            if (!scratchFile.renameTo(file)) {
                throw IOException(
                    "Unable to rename $scratchFile." +
                        "This likely means that there are multiple instances of DataStore " +
                        "for this file. Ensure that you are only creating a single instance of " +
                        "datastore for this file."
                )
            }
        } catch (ex: IOException) {
            if (scratchFile.exists()) {
                scratchFile.delete() // 忽略删除失败
            }
            throw ex
        }
    }

}

在 transformAndWrite 方法中,会在调用者的协程上下文(分发器)中执行 transform 操作,以修改 Preferences 的值,然后会判断当前数据和新数据是否有区别,没有区别的话,直接返回当前数据,否则调用 writeData 方法把新数据写入 pb 文件中,然后再更新下游状态流。

writeData 方法中,会创建一个临时文件,然后通过序列化器 PreferencesSerializer 把新的数据写入临时文件中,再把临时文件重命名为原有的 pb 文件名。

完整时序图

六、PreferenceDataStore 数据迁移流程

示例代码

Kotlin 复制代码
private fun sharedPreferencesMigration(
    context: Context
): List<SharedPreferencesMigration<Preferences>> {
    return listOf(SharedPreferencesMigration(context, PERF_NAME))
}

val Context.dataStore by preferencesDataStore(
    name = "settings",
    produceMigrations = ::sharedPreferencesMigration
)

在前面的示例代码中,有一个创建了 SharedPreferencesMigration 对象列表的函数,该函数的返回值会传给 preferencesDataStore 函数的 produceMigrations 中,SharedPreferencesMigration 用于把原有的 SharedPreferences 数据迁移到 PreferenceDataStore 中,在读取 DataStore 数据的时候,就会调用该列表中的迁移对象执行数据迁移。

SharedPreferencesMigration 函数

Kotlin 复制代码
/**
 * 为DataStore<Preferences>创建一个SharedPreferences迁移器。
 *
 * 如果迁移完成后SharedPreferences为空,则尝试删除它。
 *
 * @param context 用于获取SharedPreferences的上下文。
 * @param sharedPreferencesName SharedPreferences的名称。
 * @param keysToMigrate 要迁移的键列表。这些键将与其相同的值映射到datastore.Preferences。
 * 如果键已经存在于新Preferences中,则不会再次迁移键。
 * 如果键不存在于SharedPreferences中,则不会迁移键。
 * 如果未设置keysToMigrate,则将从现有的SharedPreferences中迁移所有键。
 */
@JvmOverloads // 为Java用户生成默认参数的方法。
public fun SharedPreferencesMigration(
    context: Context,
    sharedPreferencesName: String,
    keysToMigrate: Set<String> = MIGRATE_ALL_KEYS,
): SharedPreferencesMigration<Preferences> =
    if (keysToMigrate === MIGRATE_ALL_KEYS) {
        SharedPreferencesMigration(
            context = context,
            sharedPreferencesName = sharedPreferencesName,
            shouldRunMigration = getShouldRunMigration(keysToMigrate),
            migrate = getMigrationFunction()
        )
    } else {
        SharedPreferencesMigration(
            context = context,
            sharedPreferencesName = sharedPreferencesName,
            keysToMigrate = keysToMigrate,
            shouldRunMigration = getShouldRunMigration(keysToMigrate),
            migrate = getMigrationFunction()
        )
    }

SharedPreferencesMigration 函数用于创建 SharedPreferencesMigration 对象。

函数的参数如下:

  • context:用于获取 SharedPreferences 的上下文

  • sharedPreferencesName:SharedPreferences 的名称。

  • keysToMigrate:要迁移的键的集合。这些键将与其相同的值映射到新的DataStore中。如果键已经存在于新的Preferences中,则不会再次迁移键。如果键不存在于SharedPreferences中,则不会迁移键。如果未设置keysToMigrate,则将从现有的SharedPreferences中迁移所有键

函数使用了@JvmOverloads注解,这意味着会为Java用户生成带有默认参数的方法。

函数内部根据keysToMigrate的值的不同,分别执行了两种不同的迁移器创建方式:

  1. 如果keysToMigrate默认等于MIGRATE_ALL_KEYS(即迁移所有键),则调用了SharedPreferencesMigration的构造函数,并传入了contextsharedPreferencesNamegetShouldRunMigration(keysToMigrate)getMigrationFunction()作为参数

  2. 如果keysToMigrate不等于MIGRATE_ALL_KEYS(即只迁移指定的键),则调用了SharedPreferencesMigration的构造函数,并传入了contextsharedPreferencesNamekeysToMigrategetShouldRunMigration(keysToMigrate)getMigrationFunction()作为参数。

最终根据迁移方式的不同,返回相应的SharedPreferences迁移器。这个迁移器还会在迁移完成后检查SharedPreferences是否为空,如果为空,则尝试删除它。

getMigrationFunction

Kotlin 复制代码
/**
 * 获取迁移函数,用于将SharedPreferences迁移到Preferences。
 *
 * @return 挂起函数,接受共享首选项视图和当前数据,返回迁移后的Preferences。
 */
private fun getMigrationFunction(): suspend (SharedPreferencesView, Preferences) -> Preferences =
    { sharedPrefs: SharedPreferencesView, currentData: Preferences ->
        // prefs.getAll is already filtered to our key set, but we don't want to overwrite
        // already existing keys.
        val currentKeys = currentData.asMap().keys.map { it.name }

        // 过滤出在currentData中不存在的SharedPreferences键值对
        val filteredSharedPreferences =
            sharedPrefs.getAll().filter { (key, _) -> key !in currentKeys }

        // 将currentData转换为可变的Preferences实例
        val mutablePreferences = currentData.toMutablePreferences()

        // 遍历过滤后的SharedPreferences键值对,将其迁移到mutablePreferences中
        for ((key, value) in filteredSharedPreferences) {
            when (value) {
                is Boolean -> mutablePreferences[
                    booleanPreferencesKey(key)
                ] = value
                is Float -> mutablePreferences[
                    floatPreferencesKey(key)
                ] = value
                is Int -> mutablePreferences[
                    intPreferencesKey(key)
                ] = value
                is Long -> mutablePreferences[
                    longPreferencesKey(key)
                ] = value
                is String -> mutablePreferences[
                    stringPreferencesKey(key)
                ] = value
                is Set<*> -> {
                    @Suppress("UNCHECKED_CAST")
                    mutablePreferences[
                        stringSetPreferencesKey(key)
                    ] = value as Set<String>
                }
            }
        }

        // 将mutablePreferences转换回Preferences并返回
        mutablePreferences.toPreferences()
    }

getMigrationFunction()是一个挂起函数,用于将SharedPreferences中的数据迁移到Preferences中。函数内部首先获取当前Preferences中已存在的键,然后过滤出在当前Preferences中不存在的SharedPreferences键值对。然后,根据值的类型将这些键值对迁移到可变的Preferences实例中,最后将可变的Preferences实例转换回不可变的Preferences并返回。

最后,MIGRATE_ALL_KEYS是一个内部常量,表示要迁移所有键的集合。

getShouldMigration

Kotlin 复制代码
private fun getShouldRunMigration(keysToMigrate: Set<String>): suspend (Preferences) -> Boolean =
    { prefs ->
        // 获取当前Preferences中已存在的所有键的名称
        val allKeys = prefs.asMap().keys.map { it.name }

        if (keysToMigrate === MIGRATE_ALL_KEYS) {
            // 如果要迁移的键集合是默认值(MIGRATE_ALL_KEYS),则需要执行迁移
            true
        } else {
            // 判断要迁移的键集合中是否存在任何未在当前Preferences中的键
            keysToMigrate.any { it !in allKeys }
        }
    }
    
internal val MIGRATE_ALL_KEYS: Set<String> = mutableSetOf()

这个函数的作用是获取一个判断是否应该执行迁移的挂起函数。函数接受一个键集合(keysToMigrate)作为参数,并返回一个挂起函数,该函数接受一个Preferences实例,并返回一个布尔值,指示是否应该执行迁移。

函数内部首先通过prefs.asMap().keys获取了当前Preferences中已存在的所有键的集合,并通过.map { it.name }将这些键映射为键的名称。

接下来,通过判断keysToMigrate是否等于默认值(MIGRATE_ALL_KEYS)来决定是否需要执行迁移。如果keysToMigrate等于MIGRATE_ALL_KEYS,表示要迁移的键集合是默认的迁移所有键,则返回true,表示需要执行迁移。

如果keysToMigrate不等于MIGRATE_ALL_KEYS,表示要迁移指定的键集合,则使用keysToMigrate.any { it !in allKeys }判断是否存在任何未在当前Preferences中的键。如果存在未迁移的键,则返回true,表示需要执行迁移。如果所有要迁移的键都已经存在于当前Preferences中,则返回false,表示可以跳过迁移。

SharedPreferencesMigration

Kotlin 复制代码
/**
 * 用于从SharedPreferences迁移到DataStore的DataMigration实例。
 */
public class SharedPreferencesMigration<T>
private constructor(
    produceSharedPreferences: () -> SharedPreferences,
    keysToMigrate: Set<String>,
    private val shouldRunMigration: suspend (T) -> Boolean = { true },
    private val migrate: suspend (SharedPreferencesView, T) -> T,
    private val context: Context?,
    private val name: String?
) : DataMigration<T> {


    @JvmOverloads // 为Java用户生成带有默认参数的构造函数。
    public constructor(
        produceSharedPreferences: () -> SharedPreferences,
        keysToMigrate: Set<String> = MIGRATE_ALL_KEYS,
        shouldRunMigration: suspend (T) -> Boolean = { true },
        migrate: suspend (SharedPreferencesView, T) -> T
    ) : this(
        produceSharedPreferences,
        keysToMigrate,
        shouldRunMigration,
        migrate,
        context = null,
        name = null
    )

}

这段代码定义了一个用于从SharedPreferences迁移到DataStore的DataMigration实例。

SharedPreferencesMigration 类是一个泛型类,可以由类型参数T进行实例化。它实现了 DataMigration<T> 接口。

构造函数定义如下:

Kotlin 复制代码
private constructor(
    produceSharedPreferences: () -> SharedPreferences,
    keysToMigrate: Set<String>,
    private val shouldRunMigration: suspend (T) -> Boolean = { true },
    private val migrate: suspend (SharedPreferencesView, T) -> T,
    private val context: Context?,
    private val name: String?
)

构造函数接受以下参数:

  • produceSharedPreferences:用于返回要迁移的SharedPreferences实例的Lambda函数。

  • keysToMigrate:要迁移的键的集合。这些键将与其对应的值映射到datastore.Preferences。如果新的Preferences中已存在该键,键将不会再次迁移。如果SharedPreferences中不存在该键,它将不会被迁移。如果未设置keysToMigrate,将从现有SharedPreferences迁移所有键。

  • shouldRunMigration:一个挂起函数,用于判断是否应该运行迁移。默认情况下始终返回true。

  • migrate:一个挂起函数,用于将SharedPreferences映射为T类型的数据。这个函数必须是幂等的,因为可能会多次调用。它接受一个SharedPreferencesView对象,该对象是要迁移的SharedPreferences的视图(限于keysToMigrate),以及当前的数据对象T。该函数必须返回经过迁移后的数据对象T。如果SharedPreferences为空或不包含您指定的任何键,则不会运行此回调。

  • context:一个可选的Context对象。

  • name:一个可选的字符串参数。

SharedPreferencesMigration 的第二个构造函数是一个带有默认参数的重载构造函数,用于方便的Java用户生成实例。它接受以下参数:

Kotlin 复制代码
public constructor(
    produceSharedPreferences: () -> SharedPreferences,
    keysToMigrate: Set<String> = MIGRATE_ALL_KEYS,
    shouldRunMigration: suspend (T) -> Boolean = { true },
    migrate: suspend (SharedPreferencesView, T) -> T
)

它调用了上面私有构造函数,并将contextname参数设置为null

SharedPreferencesMigration#migrate

Kotlin 复制代码
public class SharedPreferencesMigration<T>
private constructor(
    produceSharedPreferences: () -> SharedPreferences,
    keysToMigrate: Set<String>,
    private val shouldRunMigration: suspend (T) -> Boolean = { true },
    private val migrate: suspend (SharedPreferencesView, T) -> T,
    private val context: Context?,
    private val name: String?
) : DataMigration<T> {

    /**
     * 使用lazy委托将SharedPreferences实例初始化。
     */
    private val sharedPrefs: SharedPreferences by lazy(produceSharedPreferences)

    /**
     * 如果用户指定了[MIGRATE_ALL_KEYS],则keySet为null,否则为keysToMigrate的可变集合。
     */
    private val keySet: MutableSet<String>? =
        if (keysToMigrate === MIGRATE_ALL_KEYS) {
            null
        } else {
            keysToMigrate.toMutableSet()
        }

    /**
     * 判断是否应该进行迁移的挂起函数实现。
     *
     * @param currentData 当前数据对象。
     * @return 如果应该进行迁移,则返回true;否则返回false。
     */
    override suspend fun shouldMigrate(currentData: T): Boolean {
        if (!shouldRunMigration(currentData)) {
            return false
        }

        return if (keySet == null) {
            sharedPrefs.all.isNotEmpty()
        } else {
            keySet.any(sharedPrefs::contains)
        }
    }

    /**
     * 迁移数据的挂起函数实现。
     *
     * @param currentData 当前数据对象。
     * @return 迁移后的数据对象。
     */
    override suspend fun migrate(currentData: T): T =
        migrate(
            SharedPreferencesView(
                sharedPrefs,
                keySet
            ),
            currentData
        )

}

SharedPreferencesMigration 类中定义了两个属性:

  • sharedPrefs:使用 lazy 委托将 SharedPreferences 实例初始化。

  • keySet:如果用户指定了 MIGRATE_ALL_KEYS ,则为 null ,否则为 keysToMigrate 的可变集合。

该类实现了DataMigration接口的两个方法:

  • shouldMigrate(currentData: T):判断是否应该进行迁移的挂起函数实现。根据shouldRunMigration函数的结果和keySet的值,决定是否进行迁移。

  • migrate(currentData: T):迁移数据的挂起函数实现。将SharedPreferences和当前数据对象传递给migrate函数进行迁移,并返回迁移后的数据对象。

总体而言,这个类是用于管理从SharedPreferences到DataStore的迁移过程的实例,并提供了相应的判断和迁移函数。

SharedPreferencesMigration#cleanUp

Kotlin 复制代码
public class SharedPreferencesMigration<T>
private constructor(
    produceSharedPreferences: () -> SharedPreferences,
    keysToMigrate: Set<String>,
    private val shouldRunMigration: suspend (T) -> Boolean = { true },
    private val migrate: suspend (SharedPreferencesView, T) -> T,
    private val context: Context?,
    private val name: String?
) : DataMigration<T> {
  
    @Throws(IOException::class)
    override suspend fun cleanUp() {
        val sharedPrefsEditor = sharedPrefs.edit()

        if (keySet == null) {
            sharedPrefsEditor.clear()
        } else {
            keySet.forEach { key ->
                sharedPrefsEditor.remove(key)
            }
        }

        if (!sharedPrefsEditor.commit()) {
            throw IOException("Unable to delete migrated keys from SharedPreferences.")
        }

        if (sharedPrefs.all.isEmpty() && context != null && name != null) {
            deleteSharedPreferences(context, name)
        }

        keySet?.clear()
    }

    private fun deleteSharedPreferences(context: Context, name: String) {
        if (Build.VERSION.SDK_INT >= 24) {
            if (!Api24Impl.deleteSharedPreferences(context, name)) {
                throw IOException("Unable to delete SharedPreferences: $name")
            }
        } else {
            // Context.deleteSharedPreferences is SDK 24+, so we have to reproduce the definition
            val prefsFile = getSharedPrefsFile(context, name)
            val prefsBackup = getSharedPrefsBackup(prefsFile)

            // Silently continue if we aren't able to delete the Shared Preferences File.
            prefsFile.delete()
            prefsBackup.delete()
        }
    }

    // ContextImpl.getSharedPreferencesPath is private, so we have to reproduce the definition
    private fun getSharedPrefsFile(context: Context, name: String): File {
        val prefsDir = File(context.applicationInfo.dataDir, "shared_prefs")
        return File(prefsDir, "$name.xml")
    }

    // SharedPreferencesImpl.makeBackupFile is private, so we have to reproduce the definition
    private fun getSharedPrefsBackup(prefsFile: File) = File(prefsFile.path + ".bak")

    @RequiresApi(24)
    private object Api24Impl {
        @JvmStatic
        @DoNotInline
        fun deleteSharedPreferences(context: Context, name: String): Boolean {
            return context.deleteSharedPreferences(name)
        }
    }
}

cleanUp方法内部,首先获取到SharedPreferences的编辑器sharedPrefsEditor,然后根据是否存在keySet来判断是否需要清空SharedPreferences的所有键值对,如果keySetnull,则调用sharedPrefsEditor.clear()方法清空键值对,否则遍历keySet,调用sharedPrefsEditor.remove(key)方法移除每个键。接着,通过调用sharedPrefsEditor.commit()方法提交编辑器的改动,如果提交失败,则抛出IOException异常。

接下来,如果SharedPreferences的所有键值对都已经被清空,并且contextname都不为null,则调用deleteSharedPreferences方法删除对应的SharedPreferences文件。最后清空keySet集合。

在该类中还有一些私有方法:

  • deleteSharedPreferences方法用于删除SharedPreferences文件。首先判断当前的Android版本是否大于等于24,如果是,则调用Api24Impl.deleteSharedPreferences方法进行删除;否则,通过手动删除SharedPreferences文件和备份文件的方式进行删除。

  • getSharedPrefsFile方法用于获取指定名称的SharedPreferences文件的路径。

  • getSharedPrefsBackup方法用于获取指定SharedPreferences文件的备份文件的路径。

最后,Api24Impl是一个内部静态类,在Android版本大于等于24时,提供了一个deleteSharedPreferences方法,该方法调用Context.deleteSharedPreferences方法进行删除。

ContextImpl#deleteSharedPreferences

Java 复制代码
/**
 * ContextImpl是Context API的通用实现,为Activity和其他应用程序组件提供基本的上下文对象。
 */
class ContextImpl extends Context {
  
    @Override
    public boolean deleteSharedPreferences(String name) {
        synchronized (ContextImpl.class) {
            final File prefs = getSharedPreferencesPath(name);
            final File prefsBackup = SharedPreferencesImpl.makeBackupFile(prefs);

            // 清除任何内存缓存
            final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
            cache.remove(prefs);

            prefs.delete();
            prefsBackup.delete();

            // 如果文件仍然存在,则操作失败
            return !(prefs.exists() || prefsBackup.exists());
        }
    }
}

ContextImplContext API 的通用实现,为Activity和其他应用程序组件提供基本的上下文对象。

该类重写了deleteSharedPreferences方法,用于删除SharedPreferences文件。具体解释如下:

  1. 在同步块中,获取指定名称的SharedPreferences文件prefs和备份文件prefsBackup

  2. 使用getSharedPreferencesCacheLocked方法获取SharedPreferences文件的缓存对象cache,然后从缓存中移除prefs

  3. 调用prefsdelete方法删除SharedPreferences文件。

  4. 调用prefsBackupdelete方法删除备份文件。

  5. 最后,通过检查文件是否仍然存在,若存在则操作失败,返回false;若不存在则操作成功,返回true

DataMigrationInitializer

Kotlin 复制代码
/**
 * 返回从DataMigrations列表创建的初始函数。
 */
internal class DataMigrationInitializer<T>() {
    companion object {
        /**
         * 从DataMigrations创建用于DataStore的初始值设定程序。
         *
         * @param migrations 包含在初始值设定程序中的迁移列表。
         * @return 包括从工厂函数返回的数据迁移的初始值设定程序。
         */
        fun <T> getInitializer(migrations: List<DataMigration<T>>):
            suspend (api: InitializerApi<T>) -> Unit = { api ->
                runMigrations(migrations, api)
            }

        private suspend fun <T> runMigrations(
            migrations: List<DataMigration<T>>,
            api: InitializerApi<T>
        ) {
            val cleanUps = mutableListOf<suspend () -> Unit>()

            api.updateData { startingData ->
                migrations.fold(startingData) { data, migration ->
                    if (migration.shouldMigrate(data)) {
                        cleanUps.add { migration.cleanUp() }
                        migration.migrate(data)
                    } else {
                        data
                    }
                }
            }

            var cleanUpFailure: Throwable? = null

            cleanUps.forEach { cleanUp ->
                try {
                    cleanUp()
                } catch (exception: Throwable) {
                    if (cleanUpFailure == null) {
                        cleanUpFailure = exception
                    } else {
                        cleanUpFailure!!.addSuppressed(exception)
                    }
                }
            }

            // 如果在清理过程中出现故障,则抛出异常。
            cleanUpFailure?.let { throw it }
        }
    }
}

在 DataStoreFactory 中,会通过 DataMigrationInitializer 的 getInitalizer() 方法创建一个初始化任务列表,并作为 SingleProcessDataStore 构造函数的 initTasksList 参数。

在 SingleProcessDataStore 的 readAndInit() 方法中,会遍历这些初始化任务,并创建 InitializerApi 对象传给这些初始化器。

DataMigrationInitializer 包含一个 getInitializer 方法和一个私有的 runMigrations 方法。

getInitializer 方法接受一个 DataMigrations 列表作为参数,并返回一个挂起函数(suspend)作为初始值设定程序。初始值设定程序接受一个 InitializerApi 对象作为参数,然后调用 runMigrations 方法来执行数据迁移。

runMigrations 方法执行以下操作:

  1. 创建一个用于存储清理操作(cleanUps)的可变列表。

  2. 调用 api.updateData 方法,将初始数据作为参数传递进去,并使用 fold 函数遍历迁移列表。对于每个迁移,如果 migration.shouldMigrate(data) 返回 true,则将清理操作 migration.cleanUp 添加到 cleanUps 列表中,并调用 migration.migrate 方法执行数据迁移操作。

  3. 接下来,使用可变变量 cleanUpFailure 来记录清理操作的故障异常。

  4. 遍历 cleanUps 列表,并尝试执行每个清理操作。如果其中一个清理操作抛出异常,将异常记录到 cleanUpFailure 中。使用 addSuppressed 方法将其他异常添加到主要异常中。

  5. 最后,如果在清理过程中有故障异常,则抛出 cleanUpFailure 异常。

参考资料

Android Developer - DataStore :developer.android.com/topic/libra...

相关推荐
拭心9 小时前
Google 提供的 Android 端上大模型组件:MediaPipe LLM 介绍
android
带电的小王12 小时前
WhisperKit: Android 端测试 Whisper -- Android手机(Qualcomm GPU)部署音频大模型
android·智能手机·whisper·qualcomm
梦想平凡12 小时前
PHP 微信棋牌开发全解析:高级教程
android·数据库·oracle
元争栈道12 小时前
webview和H5来实现的android短视频(短剧)音视频播放依赖控件
android·音视频
阿甘知识库13 小时前
宝塔面板跨服务器数据同步教程:双机备份零停机
android·运维·服务器·备份·同步·宝塔面板·建站
元争栈道14 小时前
webview+H5来实现的android短视频(短剧)音视频播放依赖控件资源
android·音视频
MuYe14 小时前
Android Hook - 动态加载so库
android
居居飒15 小时前
Android学习(四)-Kotlin编程语言-for循环
android·学习·kotlin
Henry_He18 小时前
桌面列表小部件不能点击的问题分析
android
工程师老罗18 小时前
Android笔试面试题AI答之Android基础(1)
android