关于Jetpack DataStore(Proto)的六点疑问

前言

上篇分析了DataStore(Preferences)的使用与原理,本篇接着阐述DataStore的另一种实现形式:DataStore(Proto)。
通过本篇文章,你将了解到:
关于Jetpack DataStore(Proto)的六点疑问_第1张图片

1. 为什么需要Proto?

DataStore(Preferences)对标SharedPreferences,前者是后者的进阶版。它们是基于Key-Value结构存储的,此种方式使用很方便,不过只能存储基本类型,如:Int、String、Long等,附带一个Set类型。

存储引用类型的对象

对于引用类型的数据结构并不能直接存储,想要存储它们通常是将对象转为Json字符串再将该字符串作为Value存储,而序列化和反序列化也有一定的性能损耗。

不保证类型安全

Key-Value存储并没有强制约束Value类型,在使用的过程中强转类型会有Crash的风险。我们想要像操作类的成员变量一样操作DataStore,此时DataStore(Proto)满足我们的需求。

2. Proto如何使用?

插件的引入

  1. 在build.gradle(:app Module级别)添加如下内容:

引入protobuf插件

plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id "com.google.protobuf"
}
//添加 id "com.google.protobuf"

添加DataStore/Protobuf 依赖

    implementation("androidx.datastore:datastore:1.0.0")
    implementation  "com.google.protobuf:protobuf-javalite:3.18.0"
//在dependencies闭包里添加

添加Protobuf属性

protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:3.14.0"
    }

compilation
    // for more information.
    generateProtoTasks {
        all().each { task ->
            task.builtins {
                java {
                    option 'lite'
                }
            }
        }
    }
}
  1. 在build.gradle(Project级别)添加如下内容,指定Protobuf插件地址:
    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插件:
关于Jetpack DataStore(Proto)的六点疑问_第2张图片
下载后重启Studio。

编写Protobuf对象

创建proto目录:
关于Jetpack DataStore(Proto)的六点疑问_第3张图片
以用户登录信息为例,创建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一下就会生成对应的类。
关于Jetpack DataStore(Proto)的六点疑问_第4张图片

创建序列化对象

以上仅仅是生成了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里操作Protobuf对象。

创建DataStore文件

        //指定该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()
        }
    }

从DataStore读取内容

    suspend fun observe() {
        context.dataProto.data.map {
            //it 指代LoginInfo对象
            "${it.userId}==${it.userName}"
        }.collect {
            println("data:$it")
        }
    }

最后输出:data:100==fish,说明我们写入和读取都成功了。

可以看出,我们仅仅只是操作对象(LoginInfo)就能完成引用类型的存取,很方便。

3. Proto的实现原理

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),此处不再赘述。

4. DataStore(Preferences)、DataStore(Proto)、SharedPreferences区别

从官网摘抄图示如下:
关于Jetpack DataStore(Proto)的六点疑问_第5张图片

此处简单说明一下:

异步API

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的数据变更回调在主线程。

同步API

SharedPreferences提供的commit方法即为同步方法,该方法需要等待文件写入成功后才会返回。
DataStore并没有提供同步方法,需要通过Flow的监听返回数据。

可在界面上安全调用

DataStore在协程里操作,因此对主线程来说是安全的。
而SharedPreferences会在主线程进行SP任务列表的刷新,由于等待任务执行结束与锁的存在可能会出现ANR(这也是SP被诟病的原因之一)

类型安全

下个小结分析。

5. 什么是类型安全?

在此之前先看看一个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本身又是强类型语言,因此编译器都能够检测出类型问题,是类型安全的。

6. 如何查看DataStore文件

一些小伙伴觉得DataStore没有SP好用的原因之一:
SP能够直接打开查看文件内容,而DataStore看到的一堆乱码。。

诚然,目前没有在Android Studio上直接查看DataStore文件的插件。
不过我们可以曲线救国将文件拷贝到电脑上,有Protobuf工具打开。
在Mac上可以使用 Protobuf Viewer 工具打开。

查看DataStore文件步骤:
第一步:
将文件导出到PC上:
关于Jetpack DataStore(Proto)的六点疑问_第6张图片

第二步:
使用工具查看LoginInfo文件:
关于Jetpack DataStore(Proto)的六点疑问_第7张图片
可以看出,Key类型和Value都展示出来了,还是比较清晰。

以上是查看DataStore(Proto)文件内容,再查看DataStore(Preferences)文件内容:
关于Jetpack DataStore(Proto)的六点疑问_第8张图片
同样的也比较明显。

本文基于:datastore:1.0.0,所有Demo请查看
下篇将分析Kotlin/Java 匿名内部类/Lambda的恩怨情仇,敬请关注。

您若喜欢,请点赞、关注、收藏,您的鼓励是我前进的动力

持续更新中,和我一起步步为营系统、深入学习Android/Kotlin

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 协程系列全面解读

你可能感兴趣的:(Jetpack,细水长流,kotlin,android,jetpack)