关于Jetpack DataStore(Preferences)的八点疑问

news/2024/12/4 5:36:22/

前言

DataStore是Android上一种轻量级存储方案,依据官方教程很容易就写出简易的Demo。
本篇主要是分析关于DataStore(Preferences)使用过程中的一些问题,通过问题寻找本质,反过来能更好地指导我们合理使用DataStore。
本篇内容目录:
image.png

1. DataStore如何存取数据?

DataStore有两种存储类型:Preferences(与SharedPreferences对标) 和 Proto。

为方便行文,以下所说的DataStore指的是Preferences类型。

引入依赖

在Module级别的build.gradle里引入:

implementation("androidx.datastore:datastore-preferences:1.0.0")

使用DataStore存取数据

存数据

  1. 先声明DataStore对象:
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "test")

DataStore是key-value 结构,因此在存取数据之前先定义好key的名字以及value的类型。

  1. 声明key的结构
    val myNameKey = stringPreferencesKey("name")val myAgeKey = intPreferencesKey("age")

想要在DataStore里存储姓名和年龄,其中姓名是String类型,年龄是Int类型。

  1. 存储value
    suspend fun saveData() {context.dataStore.edit {//给不同的key赋值it[myNameKey] = "fish"it[myAgeKey] = 18}}

取数据

    suspend fun queryData() {context.dataStore.data.collect {it.asMap().forEach {println("${it.key.name}, ${it.value}")}}}
//打印结果:
I/System.out: name, fish
I/System.out: age, 18

可以看出存取过程和SharedPreferences很相似,只是key的构造有些差异。

2. DataStore能存放哪些类型数据?

上面在构造DataStore的Key时,我们使用了两个函数:
stringPreferencesKey与intPreferencesKey,其中前缀指明了存储的value是什么类型。
实际上还有其它类型的value:
image.png

可以看出有7种类型:

Boolean、Double、Float、Int、Long、String、Set

3. DataStore存取是否耗时?

在存储数据时,我们都依赖于:

dataStore.data

而它是Flow类型:
image.png

而Flow必须要在协程里使用,因此我们使用了挂起函数(suspend)修饰存取函数。
同时我们也知道,挂起函数并不耗时。
当在主线程里分别调用DataStore的存取函数,并不会阻塞主线程。

image.png

值得注意的是:

  1. 存取数据的闭包的执行是在当前协程(调用saveData/queryData的协程)里执行的
  2. 假若当前是在主线程发起的存取动作,那么闭包将在主线程执行

总的来说:借助于协程的特性,DataFlow存取数据并不耗时。

4. DataStore Flow是如何设计的?

DataStore Flow是冷流还是热流?

先看DataStore的实现,主要依靠:SingleProcessDataStore。
在里面找到dataStore.data的定义:

    //定义热流private val downstreamFlow = MutableStateFlow(UnInitialized as State<T>)override val data: Flow<T> = flow {val currentDownStreamFlowState = downstreamFlow.valueif (currentDownStreamFlowState !is Data) {actor.offer(SingleProcessDataStore.Message.Read(currentDownStreamFlowState))}emitAll(//监听热流变化downstreamFlow.dropWhile {//满足条件则丢弃数据if (currentDownStreamFlowState is Data<T> ||currentDownStreamFlowState is Final<T>) {//不满足则继续流向mapfalse} else {//判断是否满足it === currentDownStreamFlowState}}.map {when (it) {//根据类型,返回不同的值is ReadException<T> -> throw it.readExceptionis Final<T> -> throw it.finalException//正常的返回值is Data<T> -> it.valueis UnInitialized -> error("This is a bug in DataStore. Please file a bug at: " +"https://issuetracker.google.com/issues/new?" +"component=907884&template=1466542")}})}

image.png

可以看出:

  1. dataStore.data 是Flow,它是冷流
  2. dataStore.data 里依靠downstreamFlow(热流)持续监听数据的变化
  3. 因此dataStore.data 可以持续监听数据的变化,当DataStore里数据发生变化时将会回调闭包

DataStore Flow与其它Flow的差异

先看普通的flow:

    suspend fun queryData2() {val flow = flow { emit("hello")}flow.collect {println(it)}println("normal flow end")}

大家猜测一下:"normal flow end"会打印吗?

再看DataStore的Flow:

    suspend fun queryData() {context.dataStore.data.collect {it.asMap().forEach {println("${it.key.name}, ${it.value}")}}println("dataStore flow end")}

再猜一下:"dataStore flow end"会打印吗?
答案是:

"normal flow end"会打印,而"dataStore flow end"永远没有机会执行

原因是DataStore Flow里依赖了热流监听数据,而热流的collect是不会退出的。
其实这也很容易想到:若是DataStore Flow的collect退出了,它就无法监听数据变化了。

5. DataStore 刷新范围?

存取影响范围

我们已经知道DataStore Flow可以监听数据的变化,假设我们一个文件里存放了很多对Key–Value,但是我们只关心其中一个或是某几个Key–Value的变化,比如现在新增一个key="score"字段:

    val myScoreKey = floatPreferencesKey("score")suspend fun queryDataV2() {context.dataStore.data.map {//只关心分数的变化    it[myScoreKey]}..collect {println("$it")}}suspend fun saveData2() {context.dataStore.edit {//只修改分数it[myScoreKey] = 99f}}

虽然文件了存放了三个字段:name、age、score,但是我们只更新了score字段,并且也仅仅监听score字段的变化。

那么问题来了:单个设置/监听某个字段会提升效率吗?
答案是:不会,因为DataStore的更新是基于单个文件的全量更新,也就是说虽然只是更改了score字段的值,写入文件的时候name/age字段值也会写入

我们换个写法来进行测试:

    suspend fun saveData2() {context.dataStore.edit {//只修改分数it[myNameKey] = "fish is perfect"}}

现在只是更改name字段,最后发现只监听了score变化的闭包也调用了。

小结:

DataStore更新和监听都是针对单个文件的全部字段

存相同的数值

还是以保存name为例:

    suspend fun saveData2() {context.dataStore.edit {//只修改分数it[myNameKey] = "fish is perfect"}}

当调用这函数两次。

问题:第二次调用的时候,还有会写文件的动作吗?
答案:不会,因为每次更新数据之前都会比对和上一次的数据是否一致,若是一致则不会再写入文件,当然也不会产生数据变化的通知

6. DataStore是线程安全的吗?

先看Demo:

    suspend fun saveData2() {context.dataStore.edit {//只修改分数it[myNameKey] = "fish is perfect3"}}GlobalScope.launch(Dispatchers.IO) {myDataStore.saveData2()}GlobalScope.launch(Dispatchers.Main) {myDataStore.saveData2()}

同时在子线程和主线程去更新DataStore的内容,这样合理吗?会有线程安全的问题产生吗?
答案:合理的、可行的,因为DataStore的读写是线程安全的

image.png

  1. 不管是读还是写,每次调用当做一次任务,若当前没有协程执行任务,则开启新协程执行任务,新协程跑在IO线程里
  2. 若是有任务在执行,则仅仅只是将任务加入到队列里,调用者返回;当上个任务执行完毕再执行该任务
  3. 因此单个DataStore读写是线程安全的。

此处的策略和线程池的实现类似,有需要的可以查看过往关于线程池设计的文章。

7. 能否创建多个DataStore实例?

我们一般会将都DataStore的操作封装起来:

class MyDataStore(val context: Context) {val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "test")suspend fun saveData2() {context.dataStore.edit {//只修改分数it[myNameKey] = "fish is perfect3"}}
}

而在Activity里的onCreate()方法调用如下:

        lifecycleScope.launch {MyDataStore(this@DataStoreActivity).saveData2()}

问题:这么写会有什么问题呢?

你可能会说,我试了没啥问题啊?进入Activity后成功写入DataStore。
那退出Activity再进入Activity试一次呢?

兴许你已经遇到Crash了:
image.png

提示不能有多个DataStore实例去操作同一个文件。

你可能又有疑问了:第一次进入Activity用的是一个DataStore实例,第二次进入Activity是另一个新的实例,第一个实例已经销毁了呀?为啥还会提示?

因为我们并不能完全确保同一时间只有一个DataStore实例在操作,若是存在不同的实例访问同一个文件,那么将会产生不可预期的脏数据。因此DataStore设计时就严格限制只能有一个实例访问同一个文件。

image.png

那么如何避免此种问题呢?很简单,只需要确保我们创建同一个文件只关联一个DataStore实例即可。

class MyDataStore(val context: Context) {companion object {val Context.dataStore: DataStore<Preferences> by preferencesDataStore(MyDataStore.javaClass.name)}
}

通过静态变量确保只有一个实例。

8. DataStore 如何获取同步数据?

DataStore的核心优势在于:

使用协程挂起函数存取数据,不阻塞UI,不像SharedPreferences可能会引发ANR。

DataStore只对外暴露了Flow,调用者需要通过Flow存取数据,也就是要求调用者要拥有协程环境。
然而我们可能面临的现实环境是:

  1. 调用者没有协程环境(针对老的代码)
  2. 调用者需要同步访问DataStore数据

第1点就不说了,有些老代码是Java代码,无法使用协程/接入协程代价较大。
第2点的场景:基础数据如登录与否存储在DataStore,而其它调用方仅仅只需要1个方法判断是否已经登录。

针对第2点需要同步方法有两种思路:

  1. 提供一个同步方法,用于获取外界关注的状态,而内部监听Flow的变化,有变化就同步到状态里, 如此一来,对于协程和Flow的使用控制在内部,外部仅仅只需要获取内存状态即可
  2. 提供一个同步方法,直接获取数据

我们来看看第二种思路的实现:

    val myNameKey = stringPreferencesKey("name")fun getName():String? {return runBlocking {context.dataStore.data.map {it[myNameKey]}.first() as? String}}

可以看出,我们提供的getName()并不是挂起函数,外界调用会一直等到数据的返回。

此处你可能会有担忧:getName()函数阻塞了,如果主线程调用不会耗时吗?

没错,你的担忧是合理的,假若该DataStore是第一次读取,那么getName()将阻塞等待DataStore将文件加载到内存,最后才会返回。
而只要读取了一次数据,那么后续将无需再次进行I/O读取,都是内存操作,无需担忧耗时问题。

对于第一次读取耗时问题,我们可以进行预加载,比如在某个时机提前加载数据。

9. DataStore 全流程

image.png

本文基于:datastore-preferences:1.0.0
下篇将分析DataStore Proto,敬请关注。

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

持续更新中,和我一起步步为营系统、深入学习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 协程系列全面解读


http://www.ppmy.cn/news/82443.html

相关文章

【CloudCompare教程】005:点云滤波处理大全

本文讲述基于cloudcompare软件的点云滤波方法及案例,包括:高斯滤波、低通滤波、双边滤波、统计滤波、CSF地面滤波等等。 文章目录 一、高斯滤波二、低通滤波三、双侧滤波四、统计滤波五、CSF地面滤波滤波(Wave filtering)是将信号中特定波段频率滤除的操作,是抑制和防止干…

基于ArcGIS Pro、Python、USLE、INVEST模型等多技术融合的生态系统服务构建生态安全格局

近年来&#xff0c;由于社会经济的快速发展和人口增长&#xff0c;社会活动对环境的压力不断增大&#xff0c;人地矛盾加剧。虽然全球各国在生态环境的建设和保护上已取得不少成果&#xff0c;但还是未从根本上转变生态环境的恶化趋势&#xff1b;生态破坏、环境退化、生物多样…

一名开发者眼中的 TiDB 与 MySQL 的选择丨TiDB Community

作者&#xff1a; 大数据模型 对制造业、银行业、通讯业了解多一点&#xff0c;关心专注国产数据库技术布道以及数据资产建设的应用实践。 导读 随着 MySQL 8.0 的发布和即将到来的 5.7 版本的停止支持&#xff0c;许多 MySQL 用户正面临升级和转型的抉择。本文为 TiDB 社区…

Elasticsearch的DSL和在RestClient中的应用

文章目录 一、Elasticsearch的DSL1.1 DSL Query的分类1.2 搜索结果处理 二、DSL在RestClient中的使用1.1 查询语法1.2 matchQuery和fuzzyQuery的区别1.3 排序和分页1.4 高亮显示 一、Elasticsearch的DSL 1.1 DSL Query的分类 Elasticsearch提供了基于JSON的DSL来定义查询。常见…

java实现微信小程序获取手机号(htts接口实现)

这篇文章记录一下自己写小程序后台时&#xff0c;如何通过https接口获取到用户手机号。大概流程如下&#xff1a; 1、获取通过认证的appId和secret&#xff1b; 2、利用appId和secret获取accessToken&#xff1b; 3、前端获取到用户的code&#xff1b; 4、通过code和accessToke…

React学习4-脚手架基本配置文件

React官方脚手架 react提供了一个用于创建react项目的脚手架库: create-react-app项目的整体技术架构为: react webpack es6 eslint 创建项目并启动 第一步&#xff0c;全局安装&#xff1a;npm i -g create-react-app 第二步&#xff0c;切换到想创项目的目录&#xff0c…

QSerialPort基操

以下是使用QSerialPort的基本步骤&#xff1a;1. 引入QSerialPort头文件 #include <QSerialPort>2. 创建QSerialPort对象 QSerialPort serialPort;3. 设置串口参数 serialPort.setPortName("COM1"); // 设置串口名称 serialPort.setBaudRate(QSerialPort::Baud…

腾讯C++二面,全程2小时追问基础!

今天给大家分享星球一位同学腾讯面经&#xff0c;主要摘取了部分一二面经&#xff0c;然后部分问题我做了补充说明~ 星球原文&#xff1a;https://t.zsxq.com/0eO4O13HV&#xff0c;已获授权 一面 1、C11有哪些新特性&#xff0c;有哪些新关键字 2、C中结构体占多少字节&…