探索 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:

def DATA_STORE_VERSION = "1.0.0"

// Preferences DataStore (类似于 SharedPreferences 的 API)
dependencies {
    implementation "androidx.datastore:datastore-preferences:$DATA_STORE_VERSION"

    // 可选 - RxJava2 支持
    implementation "androidx.datastore:datastore-preferences-rxjava2:1.0.0"

    // 可选 - RxJava3 支持
    implementation "androidx.datastore:datastore-preferences-rxjava3:1.0.0"
      
    // 可选 - 多进程依赖
    implementation "androidx.datastore:datastore-core-android:1.1.0-alpha04"
}

// 或者 - 使用以下不带 Android 依赖的库
dependencies {
    implementation "androidx.datastore:datastore-preferences-core:$DATA_STORE_VERSION"
}
    

Proto DataStore:

def DATA_STORE_VERSION = "1.0.0"

// 类型化的 DataStore (包含类型化 API,例如 Proto)
dependencies {
    implementation "androidx.datastore:datastore:$DATA_STORE_VERSION"

    // 可选 - RxJava2 支持
    implementation "androidx.datastore:datastore-rxjava2:1.0.0"

    // 可选 - RxJava3 支持
    implementation "androidx.datastore:datastore-rxjava3:1.0.0"
      
    // 可选 - 多进程依赖
    implementation "androidx.datastore:datastore-core-android:1.1.0-alpha04"
}

// 或者 - 使用以下不带 Android 依赖的库
dependencies {
    implementation "androidx.datastore:datastore-core:$DATA_STORE_VERSION"
}
    

注意:如果您在使用 datastore-preferences-core 库时使用了 Proguard,则必须在 proguard-rules.pro 文件中手动添加 Proguard 规则(https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:datastore/datastore-preferences/proguard-rules.pro),以防止数据被混淆后无法读出来。

创建 Preferences DataStore

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

// 在 Kotlin 文件的顶层处:
val Context.dataStore by preferencesDataStore(name = "settings")
从 Preference DataStore 中读取数据

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

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

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

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

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

往 Preference DataStore 写数据

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

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

1. Preference DataStore 创建过程

探索 Jetpack PreferenceDataStore 原理_第1张图片

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

探索 Jetpack PreferenceDataStore 原理_第2张图片

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

preferencesDataStore
@Suppress("MissingJvmstatic")
public fun preferencesDataStore(
    name: String,
    corruptionHandler: ReplaceFileCorruptionHandler? = null,
    produceMigrations: (Context) -> List> = { listOf() },
    scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
): ReadOnlyProperty> {
    return PreferenceDataStoreSingletonDelegate(name, corruptionHandler, produceMigrations, scope)
}

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

参数:

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

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

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

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

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

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

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

    /**
     * 获取 DataStore 实例(通过 by 关键字)
     */
    override fun getValue(thisRef: Context, property: KProperty<*>): DataStore {
        return INSTANCE ?: synchronized(lock) {
            if (INSTANCE == null) {
                // 获取应用上下文
                val applicationContext = thisRef.applicationContext

                // 创建实例
                INSTANCE = PreferenceDataStoreFactory.create(
                    // 序列化失败处理器
                    corruptionHandler = corruptionHandler,
                    
                    // 迁移
                    migrations = produceMigrations(applicationContext),
                  
                    // 上下文
                    scope = scope
                ) {
                    // 创建文件
                    applicationContext.preferencesDataStoreFile(name)
                }
            }
            INSTANCE!!
        }
    }
}

PreferenceDataStoreSingletonDelegate 实现了 ReadOnlyProperty 接口,也就是它是一个只读属性,该接口只有一个 getValue 方法。

getValue 方法中调用了 PreferenceDataStoreFactorycreate() 方法来创建委托类的单例,并且调用了 produceMigrations 方法和 preferencesDataStoreFile 方法来执行迁移的工作和创建保存偏好值的 Preferences 文件。

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

val Context.dataStore by preferencesDataStore(name = "settings")

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

// ...
public final class MainDataStoreKt {
   // $FF: synthetic field
   static final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)Reflection.property1(new PropertyReference1Impl(MainDataStoreKt.class, "dataStore", "getDataStore(Landroid/content/Context;)Landroidx/datastore/core/DataStore;", 1))};
   @NotNull
   private static final ReadOnlyProperty dataStore$delegate = PreferenceDataStoreDelegateKt.preferencesDataStore$default("settings", (ReplaceFileCorruptionHandler)null, (Function1)null, (CoroutineScope)null, 14, (Object)null);

   @NotNull
   public static final DataStore getDataStore(@NotNull Context $this$dataStore) {
      Intrinsics.checkNotNullParameter($this$dataStore, "$this$dataStore");
      return (DataStore)dataStore$delegate.getValue($this$dataStore, $$delegatedProperties[0]);
   }
}

字段和方法说明:

  • $$delegatedProperties:合成字段,用于委托属性的存储和访问。

  • dataStore$delegate:主数据存储的委托属性,通过调用preferencesDataStore$default函数获取。

  • getDataStore方法:通过传入Context参数获取主数据存储的DataStore对象。

整个代码主要用于获取主数据存储的 DataStore 对象,并提供一个对外访问的接口。

创建 PreferencesDataStore 文件
/**
 * 根据提供的上下文和名称生成 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
public object PreferenceDataStoreFactory {

    @JvmOverloads
    public fun create(
        corruptionHandler: ReplaceFileCorruptionHandler? = null,
        migrations: List> = listOf(),
        scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
        produceFile: () -> File
    ): DataStore {
        // 创建委托对象
        val delegate = DataStoreFactory.create(
            serializer = PreferencesSerializer,
            corruptionHandler = corruptionHandler,
            migrations = migrations,
            scope = scope
        ) {
            val file = produceFile()
            check(file.extension == PreferencesSerializer.fileExtension) {
                "文件:$file 的扩展名与 Preferences 文件所需的扩展名 ${PreferencesSerializer.fileExtension} 不匹配"
            }
            file
        }
        return PreferenceDataStore(delegate)
    }
}

PreferenceDataStoreFactory 的 create() 方法中,会通过 DataStoreFactory 创建 PreferencesDataStore 实例,这个方法是公开的,也就是除了用 preferencesDataStore ,我们也可以直接用 PreferencesDataStoreFactory.create 来创建 DataStore 。

方法参数说明:

  • corruptionHandler:当读取数据时遇到 CorruptionException 异常时,会调用该处理程序进行处理。

  • migrations:在访问数据之前运行的迁移操作列表。

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

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

该方法首先使用提供的参数创建一个 DataStoreFactory 委托对象,并将其传递给 PreferenceDataStore 的构造函数,最终返回一个新的 DataStore 实例。在创建 DataStoreFactory 委托对象时,会验证文件的扩展名是否与要求的扩展名匹配。

要注意的是将 DataStore 实例作为单例,不要在多个文件中声明相同文件的 dataStore 变量,以确保正确的功能和数据的一致性。

DataStoreFactory#create
/**
 * DataStore工厂类,用于创建DataStore实例。
 */
public object DataStoreFactory {
  
    /**
     * 创建DataStore实例的公共工厂方法。
     */
    @JvmOverloads // 为Java用户生成默认参数的构造函数
    public fun  create(
        serializer: Serializer,
        corruptionHandler: ReplaceFileCorruptionHandler? = null,
        migrations: List> = listOf(),
        scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
        produceFile: () -> File
    ): DataStore =
 

你可能感兴趣的:(Jetpack,android,android,jetpack)