opoojkk

Jetpack DataStore:Android应用中的现代数据存储方法

lxx
目次

在学习 DataStore 之前,需要先了解 Kotlin 属性委托 的概念。

Kotlin 属性委托简介 #

Kotlin 支持通过 属性委托 (by) 来委托属性的获取和设置逻辑。

基本规则是:如果一个对象实现了如下方法:

1
operator fun getValue(thisRef: Any?, property: KProperty<*>): A

那么就可以用 by 实例 的方式将属性委托给该对象。

示例:

1
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")

上面的写法将 DataStore 实例绑定到 Context 的扩展属性 dataStore,访问时会通过委托自动初始化。

创建 DataStore 实例 #

1
2
3
4
5
6
val EXAMPLE_COUNTER = intPreferencesKey("example_counter")

val exampleCounterFlow: Flow<Int> = context.dataStore.data
    .map { preferences ->
        preferences[EXAMPLE_COUNTER] ?: 0
    }

preferencesDataStore 方法利用 Kotlin 的属性委托创建 DataStore,并通过 double-check 机制 确保整个应用中访问到的都是同一个实例。

DataStore 是一个数据接口,它主要有两种实现:

  1. Preference DataStore:对应 SharedPreferences 风格,使用键值对存储。
  2. Proto DataStore:对应 ProtoBuf 协议,存储自定义类型,需要手动实现序列化和反序列化。

DataStore 内部关键组件 #

DataStoreImpl 是 DataStore 的核心实现类,其中关键变量有:

DataStore 状态 #

DataStore 内部状态包括:

第一次访问 DataStore 时,状态为 UnInitialized,初始化完成后才会切换到 Data 状态。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
private suspend fun readState(requireLock: Boolean): State<T> =
    withContext(scope.coroutineContext) {
        if (inMemoryCache.currentState is Final) {
            inMemoryCache.currentState
        } else {
            try {
                readAndInitOrPropagateAndThrowFailure()
            } catch (throwable: Throwable) {
                return@withContext ReadException(throwable, -1)
            }
            readDataAndUpdateCache(requireLock)
        }
    }

readAndInit 的核心逻辑是 只执行一次

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
internal abstract class RunOnce {
    private val runMutex = Mutex()
    private val didRun = CompletableDeferred<Unit>()
    protected abstract suspend fun doRun()

    suspend fun runIfNeeded() {
        if (didRun.isCompleted) return
        runMutex.withLock {
            if (didRun.isCompleted) return
            doRun()
            didRun.complete(Unit)
        }
    }

    suspend fun awaitComplete() = didRun.await()
}

通过 didRun.isCompleted 判断是否已经初始化,保证首次初始化只执行一次。

读取数据流程 #

1
2
val exampleCounterFlow: Flow<Int> = context.dataStore.data
    .map { preferences -> preferences[EXAMPLE_COUNTER] ?: 0 }

写入数据流程 #

写入流程与 SharedPreferences 类似,但更加安全和异步:

1
2
3
4
5
6
lifecycleScope.launch {
    dataStore.edit { settings ->
        val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0
        settings[EXAMPLE_COUNTER] = currentCounterValue + 1
    }
}

DataStore 写入特点:

  1. 数据先更新到内存缓存。
  2. 再写入临时文件(.tmp)。
  3. 临时文件写入成功后,覆盖原文件。
  4. 通过版本号和锁机制保证多线程/多进程访问一致性。

示例核心实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
internal suspend fun writeData(newData: T, updateCache: Boolean): Int {
    var newVersion = 0
    storageConnection.writeScope {
        newVersion = coordinator.incrementAndGetVersion()
        writeData(newData)
        if (updateCache) {
            inMemoryCache.tryUpdate(Data(newData, newData.hashCode(), newVersion))
        }
    }
    return newVersion
}

DataStore 与 SharedPreferences 对比 #

特性SharedPreferencesDataStore
存储方式XML 文件Proto / Preferences Map
初始化同步解析 XML,可能导致 ANR异步解析,基于 Flow
写入onPause/onStop 阻塞磁盘写入先写 tmp 文件,再覆盖,异步安全
多线程/多进程支持有限,多线程容易出错通过锁和版本号保证一致性
类型安全仅基本类型Preferences 无类型安全,Proto 可自定义类型

Proto DataStore #

Proto DataStore 使用 Protobuf 存储数据:

多进程支持 #

DataStore 支持多进程访问,区别在于锁机制:

1
2
3
4
5
lockFileStream.getChannel().tryLock(
    /* position= */ 0L,
    /* size= */ Long.MAX_VALUE,
    /* shared= */ true
)

总结 #

DataStore 相比 SharedPreferences 的优势:

  1. 异步、安全,避免 ANR。
  2. 先写入临时文件,再覆盖原文件,保证数据一致性。
  3. 支持 Flow,实现响应式数据更新。
  4. 通过版本号和锁机制,支持多线程与多进程安全。
  5. 支持 Proto 格式,类型安全可扩展。

参考资料 #

标签:
Categories: