Android KV存储-DataStore

1,042 阅读5分钟

继上次看完 MMKV 之后,我又继续看了下 Android 官方发布的 DataStore 框架,这里来做一下记录。

DataStore介绍与使用

DataStore 是 Android 官方发布在 androidx 里面的一个 kv 存储解决方案。目的是为了替代 SharedPreferences。DataStore 按功能分为两部分,Preference DataStore 和 Proto DataStore。

  • Preference DataStore:kv访问,不需要定义数据结构,但是也不保证类型安全
  • Proto DataStore:使用者提前遵循 proto buffer 协议定义数据结构,可以保证数据安全

Preference DataStore的使用

  1. 按照官方文档指引引入 datastore-preferencesdatastore-preferences-core 的依赖
  2. 创建 DataStore 对象:

  1. 写入数据

调用 DataStore#edit, 这是一个kotlin suspend方法:

  1. 读取数据

    通过 DataStore@dtaa 获取一个 Flow,在map闭包里面会获取到 Prefernence 对象,可以通过 key 直接读取配置里面的值:

Proto DataStroe 的使用

  1. 引入 datastoredatastore-core 的依赖
  2. 引入 protobuf相关的依赖
implementation("com.google.protobuf:protobuf-javalite:3.17.3")

// app build.gradle引入插件
plugins {
    id("com.google.protobuf") version "0.9.1"
}

// app build.gradle里面加入插件配置
// 此闭包和dependencies平级
protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:3.21.9"
    }
    generateProtoTasks {
        all().forEach { task ->
            task.builtins {
                id("java") {
                    option("lite")
                }
            }
        }
    }
}
  1. 定义数据结构,使用protobuf插件自动生成相关对象的代码

在src/main/proto 下面定义

build之后会自动生成 Settings 对象:

  1. 创建 DataStore 对象

首先针对Settings定义一个 Serializer:

  然后获取 DataStore 对象的时候 fileName 传入的是pb文件

5. 写入数据

 调用 DataStore#updateData 方法:

  1. 读取数据

    这里也是通过 DataStore@data 获取一个 Flow,但是 map 闭包里提供的是一个 Settings 对象:

实现原理

接下来我们通过实现原理来琢磨为什么Android团队会推出一个这个来建议替代SharedPreferences:

创建DataStore

通过kotlin委托属性来获得一个 DataStore 对象,获取的委托对象是一个ReadOnlyProperty,在getValue方法里面会创建DataStore对象:

create方法会创建PreferenceDataStore对象,PreferenceDataStore对象是一个典型的委托模式:

实际上最里面一层创建的是DataStoreImpl:

这么设计的目的是让DataStoreImpl写入数据的时候,可以先执行外面定义的数据转化:transform

写入数据

我们从DataStoreImpl的updateData入手分析写入数据的流程。核心逻辑就是给writeActor提交一个Update的消息:

writeActor里面会有一个一直在处理更新消息的 Channel,当消息处理的时候调用 handleUpdate 方法:

transformAndWrite则会在写文件之前加上锁

接着调用trransform之后调用writeData方法

这里调用顺序为

  1. 调用 StorageConnection 的 writeScope
  2. 调用InterProcessCoordinator的incrementAndGetVersion升级版本
  3. 调用 StorageConnection 的 writeData,并且更新缓存

StorageConenction和InterProcessCoordinator都是在初始化DataStore对象的时候创建的。

StorageConnection的实现类有两个,命名分别是File和Okio:

这里传入的是OkioStorageConenction,writeScope实现如下,实际上就是使用 OKio 的api来创建(获取)并写入文件。FileStorageConnection对应的则是java File api的实现

InterProcessCoordinator则是一个协调器,分别有单进程和多进程的实现,如果你需要DataStore支持多进程,需要手动传入 MultiProcessCoordinator:

默认情况下单进程加锁使用的协程的 Mutex,版本+1就是一个 AtomicInt +1:

多进程版本则通过native层实现版本+1,通过文件锁实现加锁:

接着就是 OkioStorageConnection调用writeData把数据写入文件:

这里写入数据调用的就是 Serializer 的 writeTo 方法。当使用自定义协议数据的时候,这个serializer由使用者定义,如果是preference的时候,使用内置的 PreferencesSerializer:

我查询了一下PreferenceMap的优势在于他读写数据不需要把文件内容全部读出,效率非常高

读取数据

获取DataStore的数据我们一般是通过data返回一个flow对象。

flow内部下面则是一个emitAll调用

flow里面在我们collect这个flow的时候,就会触发逻辑,也就是readState、emitAll。

emitAll则会在 inMemoryCache.flow 在数据发生变化的时候,触发data flow的更新。这样我们不仅能在第一次使用data的时候收到DataStore最新的值,也能在缓存更新的时候监听到最新值的变化。

总结

到这里DataStore的基本原理已经摸清楚了,通过他的实现我们基本能整理出他对比SharedPreferences的优点:

  • DataStore都在协程里面运行,读写文件的时候使用的也是协程的Mutex,比较轻量级。不会出现SharedPreferences出现多线程读写死锁的问题。
  • DataStore支持proto协议对对象进行读写,类型安全,而SharedPreferences只能支持单个配置读写。
  • DataStore通过protobuffer写文件的时候不需要像SharedPreferences一样把数据全部读出来放在内存,也不需要全部内容重新写回文件,性能更好。虽然性能没有MMKV使用mmap那么强,但是也非常的稳定。

至于缺点,DataStore也是有一些不足的,例如没有同步的api,java使用的话要搭配rxjava等等。不过如果项目主体是kotlin的话,这倒也不是什么问题。

最后一点是关于DataStore的学习价值,我们可以看到他的源码里面有关于:

  • Kotlin channel的使用
  • Kotlin flow的使用
  • Okio的使用
  • Java NIO 文件锁的使用

这些都值得我们学习一下。