上篇分析了DataStore(Preferences)的使用与原理,本篇接着阐述DataStore的另一种实现形式:DataStore(Proto)。
通过本篇文章,你将了解到:
DataStore(Preferences)对标SharedPreferences,前者是后者的进阶版。它们是基于Key-Value结构存储的,此种方式使用很方便,不过只能存储基本类型,如:Int、String、Long等,附带一个Set类型。
对于引用类型的数据结构并不能直接存储,想要存储它们通常是将对象转为Json字符串再将该字符串作为Value存储,而序列化和反序列化也有一定的性能损耗。
Key-Value存储并没有强制约束Value类型,在使用的过程中强转类型会有Crash的风险。我们想要像操作类的成员变量一样操作DataStore,此时DataStore(Proto)满足我们的需求。
plugins {
id 'com.android.application'
id 'kotlin-android'
id "com.google.protobuf"
}
//添加 id "com.google.protobuf"
implementation("androidx.datastore:datastore:1.0.0")
implementation "com.google.protobuf:protobuf-javalite:3.18.0"
//在dependencies闭包里添加
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.14.0"
}
compilation
// for more information.
generateProtoTasks {
all().each { task ->
task.builtins {
java {
option 'lite'
}
}
}
}
}
dependencies {
classpath "com.android.tools.build:gradle:4.2.0"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.17'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
//引入classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.17'
想要往DataStore里存取数据,先要预先定义好数据结构,此时需要定义Protobuf对象。
为了更好的编写Protobuf文件,在此之前我们需要给Android Studio 下载Protobuf插件:
下载后重启Studio。
创建proto目录:
以用户登录信息为例,创建login_info.proto文件,编写内容:
syntax = "proto3";
//指定生成类的包名
option java_package = "com.fish.kotlindemo.test";
option java_multiple_files = true;
//message 可以类比class
//编译后,自动生成对应的class
message LoginInfo {
//定义字段, 1,2表示字段顺序而非具体值
int64 userId = 1;
string userName = 2;
}
如上,我们希望存储的是用户id和用户名,此时仅仅只是配置了相关信息,需要build一下就会生成对应的类。
以上仅仅是生成了Protobuf对象,和DataStore还没有任何联系,我们需要指定该Protobuf对象是如何存储到DataStore里的,而我们知道存储到文件势必涉及到文件读写,因此我们需要告知读写的方式(类似使用Parcelable(Java)时需要重写write和read方法)
定义序列化对象:
object LoginInfoSerializer : Serializer<LoginInfo> {
//默认值
override val defaultValue: LoginInfo
get() = LoginInfo.getDefaultInstance()
//如何从文件里读取Protobuf内容
override suspend fun readFrom(input: InputStream): LoginInfo {
return LoginInfo.parseFrom(input)
}
//将Protobuf写入到文件
override suspend fun writeTo(t: LoginInfo, output: OutputStream) {
t.writeTo(output)
}
}
准备工作就绪,接下来看看如何在DataStore里操作Protobuf对象。
//指定该DataStore存储对象为LoginInfo
val Context.dataProto: DataStore<LoginInfo> by dataStore(
//文件名,存储在/data/data/包名/files/datastore下
fileName = "login_info",
//指定序列化器,负责将对象序列化/反序列化-到/从文件
serializer = LoginInfoSerializer
)
suspend fun updateDataStore(userName: String, userId:Long) {
context.dataProto.updateData { loginInfo ->
//loginInfo为生成的类的对象
loginInfo.toBuilder()
//给字段赋值
.setUserName(userName)
.setUserId(userId)
//返回LoginInfo
.build()
}
}
suspend fun observe() {
context.dataProto.data.map {
//it 指代LoginInfo对象
"${it.userId}==${it.userName}"
}.collect {
println("data:$it")
}
}
最后输出:data:100==fish,说明我们写入和读取都成功了。
可以看出,我们仅仅只是操作对象(LoginInfo)就能完成引用类型的存取,很方便。
DataStore(Preferences)与DataStore(Proto)的实现原理核心都是一致的,区别在于序列化器的选择。
Preferences使用的序列化器默认是:
internal object PreferencesSerializer : Serializer<Preferences> {
val fileExtension = "preferences_pb"
override val defaultValue: Preferences
get() {
return emptyPreferences()
}
@Throws(IOException::class, CorruptionException::class)
override suspend fun readFrom(input: InputStream): Preferences {
//从文件读取内容
val preferencesProto = PreferencesMapCompat.readFrom(input)
//构造preferences列表
val mutablePreferences = mutablePreferencesOf()
preferencesProto.preferencesMap.forEach { (name, value) ->
//根据类型,填充Key-Value
addProtoEntryToPreferences(name, value, mutablePreferences)
}
//返回带有Key-Value的结构
return mutablePreferences.toPreferences()
}
@Throws(IOException::class, CorruptionException::class)
override suspend fun writeTo(t: Preferences, output: OutputStream) {
//转为Map,Map里就是Key-Value结构
val preferences = t.asMap()
val protoBuilder = PreferencesProto.PreferenceMap.newBuilder()
for ((key, value) in preferences) {
//取出Key-Value
protoBuilder.putPreferences(key.name, getValueProto(value))
}
//写入到文件
protoBuilder.build().writeTo(output)
}
}
可以看出Preferences序列化的目标是Key-Value结构,而DataStore(Proto)根据不同的Protobuf生成的对象定义具体的序列化器。
其它核心原理请参照之前分析的DataStore(Preferences),此处不再赘述。
此处简单说明一下:
DataStore是基于Flow进行的读数据,对文件的IO操作是在子线程,而该Flow可以在主线程监听数据的变化,因此天然就是支持异步API。
SharedPreferences 可能很少使用监听,简单的监听如下:
sp = context.getSharedPreferences("mysp", Context.MODE_PRIVATE)
sp?.registerOnSharedPreferenceChangeListener { sharedPreferences, key ->
//监听回调,key为变化的条目
val changed = sharedPreferences.getString(key, "onListener")
}
SharedPreferences的数据变更回调在主线程。
SharedPreferences提供的commit方法即为同步方法,该方法需要等待文件写入成功后才会返回。
DataStore并没有提供同步方法,需要通过Flow的监听返回数据。
DataStore在协程里操作,因此对主线程来说是安全的。
而SharedPreferences会在主线程进行SP任务列表的刷新,由于等待任务执行结束与锁的存在可能会出现ANR(这也是SP被诟病的原因之一)
下个小结分析。
在此之前先看看一个Demo:
fun ts() {
var str : String? = null
str = 11
}
猜猜是否能编过?答案是否定的。
因为Kotlin是强类型语言,声明的str为String类型,那么就只能接收String或是子类的值。
引申到SharedPreferences和DataStore存储里。
继续看SharedPreferences的读写Demo:
fun saveSP() {
sp?.edit {
putString("name", "fish${Random().nextInt(10000)}")
putInt("age", 19)
}
}
往SP里写入Int类型数据和String类型数据。
读取方式如下:
fun getSP() {
val name = sp?.getString("name", "test")
val age = sp?.getString("age", "test")
}
这里编译会有问题吗?答案是否定的。
运行会有问题吗?答案是肯定的。
因为我们写入的age是Int类型,而试图以String类型读取,Int肯定不能强转为String,因此会Crash。
由于在编译期没有提醒我们,使得这些问题容易暴露的运行时,因此我们说SharedPreferences不是类型安全的。
再看看DataStore(Preferences):
一样的套路,先看写入文件:
val myNameKey = stringPreferencesKey("name")
val myAgeKey = intPreferencesKey("age")
suspend fun saveData() {
context.dataStore.edit {
//给不同的key赋值
it[myNameKey] = "fish"
it[myAgeKey] = "14"
}
}
这里的编译会有问题吗?答案是肯定的。
因为myAgeKey定义泛型类型为Int,因此只能给它赋Int类型的值。
再看读取文件:
suspend fun getData() {
context.dataStore.data.collect {
val name:String? = it[myNameKey]
val age:String = it[myAgeKey]
}
}
这里编译会有问题吗?答案是肯定的。
age是Int类型,不能强转为String。
到这里你可能就比较疑惑了,既然DataStore(Preferences)读写都会在编译期检测类型,那么它应该算类型安全的?
其实不然。
在定义DataStore(Preferences) Key-Value结构时,Key的类型是泛型,因此会根据实际的类型进行约束。
当我们需要遍历DataStore(Preferences)文件里所有的字段时,可能会这么写
suspend fun getData() {
context.dataStore.data.collect {
it.asMap().forEach {
val vaule = it.value as String
}
}
}
这里编译会有问题吗?答案是否定的。
运行会有问题吗?答案是肯定的。
因为age是Int类型,不能转为String。
查看asMap方法可知:
public abstract fun asMap(): Map<Key<*>, Any>
value 是Any类型的。
综上所述,DataStore(Preferences)也不是类型安全的。
而DataStore(Proto)完全是基于对象的操作,Kotlin本身又是强类型语言,因此编译器都能够检测出类型问题,是类型安全的。
一些小伙伴觉得DataStore没有SP好用的原因之一:
SP能够直接打开查看文件内容,而DataStore看到的一堆乱码。。
诚然,目前没有在Android Studio上直接查看DataStore文件的插件。
不过我们可以曲线救国将文件拷贝到电脑上,有Protobuf工具打开。
在Mac上可以使用 Protobuf Viewer 工具打开。
查看DataStore文件步骤:
第一步:
将文件导出到PC上:
第二步:
使用工具查看LoginInfo文件:
可以看出,Key类型和Value都展示出来了,还是比较清晰。
以上是查看DataStore(Proto)文件内容,再查看DataStore(Preferences)文件内容:
同样的也比较明显。
本文基于:datastore:1.0.0,所有Demo请查看
下篇将分析Kotlin/Java 匿名内部类/Lambda的恩怨情仇,敬请关注。
1、Android各种Context的前世今生
2、Android DecorView 必知必会
3、Window/WindowManager 不可不知之事
4、View Measure/Layout/Draw 真明白了
5、Android事件分发全套服务
6、Android invalidate/postInvalidate/requestLayout 彻底厘清
7、Android Window 如何确定大小/onMeasure()多次执行原因
8、Android事件驱动Handler-Message-Looper解析
9、Android 键盘一招搞定
10、Android 各种坐标彻底明了
11、Android Activity/Window/View 的background
12、Android Activity创建到View的显示过
13、Android IPC 系列
14、Android 存储系列
15、Java 并发系列不再疑惑
16、Java 线程池系列
17、Android Jetpack 前置基础系列
18、Android Jetpack 易学易懂系列
19、Kotlin 轻松入门系列
20、Kotlin 协程系列全面解读