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 函数的源码:


@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 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 (tT) -> 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 函数:


@Throws(IOException::class, CorruptionException::class) 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.BundleInsideHelperimport androidx.build.LibraryGroupsimport 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 这个东西,它是个自定义插件,看看实现:


@JvmStatic 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的主要内容,如果未能解决你的问题,请参考以下文章

Android Jetpack组件 DataStore的使用和简单封装

Jetpack DataStore 你总要了解一下吧?

Jetpack DataStore 你总要了解一下吧?

Jetpack DataStore 你总要了解一下吧?

Android Jetpack系列之DataStore

Android Jetpack之DataStore指南