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

本文介绍了Android中DataStore的Proto实现,用于存储引用类型数据,避免了SharedPreferences的限制。文章详细讲解了为何需要Proto,如何配置和使用Proto,包括创建Proto对象、序列化器以及在DataStore中的操作。同时,对比了DataStore(Preferences)和SharedPreferences的异同,强调了类型安全问题,并展示了如何查看DataStore文件内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

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

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插件:

下载后重启Studio。

编写Protobuf对象

创建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里操作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区别

从官网摘抄图示如下:

此处简单说明一下:

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

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

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

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

同样的也比较明显。

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

作者:小鱼人爱编程
链接:https://juejin.cn/post/7250112956700573753
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值