Jetpack DataStore 源码分析—— Preferences
Posted Kotlin 维修车间
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Jetpack DataStore 源码分析—— Preferences相关的知识,希望对你有一定的参考价值。
我们的 KMM 工程迫切需要一种 android/ios 两端均能支持的 KV 存储框架,用于支持简单的数据本地存储场景。如果只是为了眼前的需要,我们可以简单的将 Android 的 SharedPreference 与 iOS 的 NSUserDefault 进行一次 common 层的抽象与封装从而达到目的。但 SharedPrefenence 在 Android 上效率低下,在 Google 的 Jetpack DataStore 推出以后已经趋于被淘汰的态势,因此我们决定将 Jetpack DataStroe 改写为跨平台版本,从而在未来相当长的一段时间内满足我们的需求。
DataStrore 分为两类,一类是与 SharedPreference 类似的 Preference,用于使用 KV 存取基本类型;另一类是基于 protobuf 的 Proto,用于存取对象并保证类型安全。本文我们先分析 Preference。
根据官方教程,通常我们需要创建一个 DataStore 对象提供给整个工程使用:
// At the top level of your kotlin file:
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
preferencesDataStore 函数比较简单,内部以一个委托类持有单例的方式向外提供了 DataStore 对象,这里源码就不贴了,我们贴一下 DataStore 单例的创建过程:
INSTANCE = PreferenceDataStoreFactory.create(
corruptionHandler = corruptionHandler,
migrations = produceMigrations(thisRef.applicationContext),
scope = scope
{
thisRef.preferencesDataStoreFile(name)
}
从代码可以轻易看出,DataStore 以工厂方法创建,我们看下 create 函数的源码:
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 extension for file: $file does not match required extension for" +
" Preferences file: ${PreferencesSerializer.fileExtension}"
}
file
}
return PreferenceDataStore(delegate)
}
先解释一下参数,从源码注释得知,corruptionHandler 是一个用户可自定义的异常处理器,用以在反序列化失败的时候处理异常时使用,默认为 null;migrations 注释上说是一个必定会运行一次以上的操作,从名称上看和数据迁移有关,具体细节等下我们再看;Scope 是我们的老朋友了,协程作用域,无需废话;produceFile 是一个函数类型,用以返回一个文件,这里盲猜是存储数据的文件,等会儿可以再验证一下。
从 create 函数的实现中看出,文件完全由 produceFile 参数产生,create 的内部只是对文件的扩展名做了一下校验。那文件实际生成代码其实就在 INSTANCE 单例创建的调用处,核心就是这一行:
thisRef.preferencesDataStoreFile(name)
扒开它内部的层层套娃发现,它非常普通地创建了一个 File:
File(applicationContext.filesDir, "datastore/$fileName")
这个文件实际上都是被 delegate 内部拿去使用了,delegate 作为委托者提供给了 PreferenceDataStore 这个类:
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)
(transformed as MutablePreferences).freeze()
transformed
}
}
}
所以真正的文件 IO 必定在 delegate 变量对应的实现类的内部。
顺着工厂方法进去,我们找到了最终的实现者:SingleProcessDataStore。
直接找到它的 updateData 方法:
override suspend fun updateData(transform: suspend (t: T) -> T): T {
val ack = CompletableDeferred<T>()
val currentDownStreamFlowState = downstreamFlow.value
val updateMsg = Message.Update(transform, ack, currentDownStreamFlowState, coroutineContext)
actor.offer(updateMsg)
return ack.await()
}
Message 是个密封类,Update 是它的子类,Message 的另一个子类是 Read。Update 和 Read 表示对文件的两种操作,一个写,一个读。
actor 的类型是 SimpleActor。它的内部使用 Channel 来接收和发送消息,其实说白了就是一个封装过的消息队列。真正处理读和写逻辑的还是在 SingleProcessDataStore 这个类中的 handleRead 和 handleUpdate 两个方法内。
先看 handleRead,内部也是经过了一连串的函数调用:handleRead -> readAndInitOrPropagrateAndThrowFailure -> readAndInit -> readDataAndHandleCorruption -> readData。为什么要有这么多层函数调用?主要还是 Google 的代码真的是非常规范,每个函数都符合单一职责原则,这其中的核心虽然是读取数据,但是每层函数调用也处理了诸如传递数据、处理异常、流转状态等不同的逻辑。
最后的 readData 函数是这样的:
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
}
}
真正的读取是靠 Serializer 来做的,而读取 Preference 的 Serializer 的具体实现类是:PreferencesSerializer。
看一下刚才被调用的 readFrom 函数:
override suspend fun readFrom(input: InputStream): Preferences {
val preferencesProto = PreferencesMapCompat.readFrom(input)
val mutablePreferences = mutablePreferencesOf()
preferencesProto.preferencesMap.forEach { (name, value) ->
addProtoEntryToPreferences(name, value, mutablePreferences)
}
return mutablePreferences.toPreferences()
}
接着看看 PreferencesMapCompat.readFrom:
class PreferencesMapCompat {
companion object {
fun readFrom(input: InputStream): PreferencesProto.PreferenceMap {
return try {
PreferencesProto.PreferenceMap.parseFrom(input)
} catch (ipbe: InvalidProtocolBufferException) {
throw CorruptionException("Unable to parse preferences proto.", ipbe)
}
}
}
}
核心是 PreferencesProto.preferenceMap.parseFrom(imput) 这一行。
然后问题就来了,PreferencesProto 这个类在 DataStore 的源码路径下怎么也找不到。然后我看了下它工程的 gradle 配置:
import static androidx.build.dependencies.DependenciesKt.*
import androidx.build.BundleInsideHelper
import androidx.build.LibraryGroups
import androidx.build.Publish
plugins {
id("AndroidXPlugin")
id("kotlin")
}
BundleInsideHelper.forInsideJar(
project,
/* from = */ "com.google.protobuf",
/* to = */ "androidx.datastore.preferences.protobuf"
)
dependencies {
bundleInside(project(
path: ":datastore:datastore-preferences-core:datastore-preferences-proto",
configuration: "export"
))
api(KOTLIN_STDLIB)
api(project(":datastore:datastore-core"))
testImplementation(JUNIT)
testImplementation(KOTLIN_COROUTINES_TEST)
testImplementation(KOTLIN_TEST)
}
重点是 BundleInsideHelper.forInsideJar 这个东西,它是个自定义插件,看看实现:
fun Project.forInsideJar(from: String, to: String) {
val bundle = configurations.create("bundleInside")
val repackage = configureRepackageTaskForType("jar", from, to, bundle)
dependencies.add("compileOnly", files(repackage.flatMap { it.archiveFile }))
dependencies.add("testImplementation", files(repackage.flatMap { it.archiveFile }))
val jarTask = tasks.named("jar")
jarTask.configure {
it as Jar
it.from(repackage.map { files(zipTree(it.archiveFile.get().asFile)) })
}
configurations.getByName("apiElements") {
it.outgoing.artifacts.clear()
it.outgoing.artifact(
jarTask.flatMap { jarTask ->
jarTask as Jar
jarTask.archiveFile
}
)
}
configurations.getByName("runtimeElements") {
it.outgoing.artifacts.clear()
it.outgoing.artifact(
jarTask.flatMap { jarTask ->
jarTask as Jar
jarTask.archiveFile
}
)
}
}
说实话,没有完全看明白,但大意应该是把一个 jar 包里的代码替换包名后重新打进当前 bundle?根据它传入的参数,它把 com.google.protobuf 替换成了 androidx.datastore.preferences.protobuf。
com.google.protobuf 是 Google 开发的用于序列化 Protobuf 的 Java 库,它已经不属于 Datastore 乃至 Jetpack 的范畴,在这里先不继续深入分析它了,不过幸运的是,从官网来看,它有对应的 Objective-C 版本,所以这一块的代码如果要移植到 KMM 理论上也是有办法的,而不需要我们用 Kotlin 完全重写。
最后 writeTo 方法和 readFrom 也是类似的,都用到了 com.google.protobuf 里的 API,这里也不赘述了。
总结
虽然本文流水账式的过了一遍 DataStore 的源码,但是还是要装模作样的总结下。
对暴露给外部的 API 来说,如果我们要做一个 KMM 版的 DataStore 几乎可以完全照抄 DataStore 的代码,唯一平台相关的就是 Java 的 File 类,我们稍加改动,把它变成 String 类型的文件路径即可,在内部有大量平台相关实现的地方把它统一处理下。然后就是 InputStream,也是 JVM 的 API,至于怎么抽象它我们要研究一下 Objective-C 版的 Protobuf 库的 API,最后的核心实现就是也就是文件相关的 IO 部分我们通过 common 层抽象的方式把 Java 和 Objective-C 版的 Protobuf 库封装一下理论上可以搞定。
那么下回再见。
以上是关于Jetpack DataStore 源码分析—— Preferences的主要内容,如果未能解决你的问题,请参考以下文章