继上次看完 MMKV 之后,我又继续看了下 Android 官方发布的 DataStore 框架,这里来做一下记录。
DataStore介绍与使用
DataStore 是 Android 官方发布在 androidx 里面的一个 kv 存储解决方案。目的是为了替代 SharedPreferences。DataStore 按功能分为两部分,Preference DataStore 和 Proto DataStore。
- Preference DataStore:kv访问,不需要定义数据结构,但是也不保证类型安全
- Proto DataStore:使用者提前遵循 proto buffer 协议定义数据结构,可以保证数据安全
Preference DataStore的使用
- 按照官方文档指引引入
datastore-preferences和datastore-preferences-core的依赖 - 创建 DataStore 对象:
- 写入数据
调用 DataStore#edit, 这是一个kotlin suspend方法:
-
读取数据
通过 DataStore@dtaa 获取一个 Flow,在
map闭包里面会获取到 Prefernence 对象,可以通过 key 直接读取配置里面的值:
Proto DataStroe 的使用
- 引入
datastore和datastore-core的依赖 - 引入
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")
}
}
}
}
}
- 定义数据结构,使用protobuf插件自动生成相关对象的代码
在src/main/proto 下面定义
build之后会自动生成 Settings 对象:
- 创建 DataStore 对象
首先针对Settings定义一个 Serializer:
然后获取 DataStore 对象的时候 fileName 传入的是pb文件
5. 写入数据
调用 DataStore#updateData 方法:
-
读取数据
这里也是通过 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方法
这里调用顺序为
- 调用 StorageConnection 的 writeScope
- 调用InterProcessCoordinator的incrementAndGetVersion升级版本
- 调用 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 文件锁的使用
这些都值得我们学习一下。