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

前言

上篇分析了DataStore(Preferences)的使用与原理,本篇接着阐述DataStore的另一种实现形式:DataStore(Proto)。
通过本篇文章,你将了解到:


image.png

1. 为什么需要Proto?

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

存储引用类型的对象

对于引用类型的数据结构并不能直接存储,想要存储它们通常是将对象转为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插件:


image.png

下载后重启Studio。

编写Protobuf对象

创建proto目录:


image.png

以用户登录信息为例,创建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一下就会生成对应的类。


image.png

创建序列化对象

以上仅仅是生成了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区别

从官网摘抄图示如下:


image.png

此处简单说明一下:

异步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上:

image.png

第二步:
使用工具查看LoginInfo文件:

image.png

可以看出,Key类型和Value都展示出来了,还是比较清晰。

以上是查看DataStore(Proto)文件内容,再查看DataStore(Preferences)文件内容:


image.png

同样的也比较明显。

本文基于: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 协程系列全面解读

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,233评论 6 495
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,357评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,831评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,313评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,417评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,470评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,482评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,265评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,708评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,997评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,176评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,827评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,503评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,150评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,391评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,034评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,063评论 2 352

推荐阅读更多精彩内容