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

news2024/7/4 6:28:00

前言

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.value

        if (currentDownStreamFlowState !is Data) {
            actor.offer(SingleProcessDataStore.Message.Read(currentDownStreamFlowState))
        }

        emitAll(
            //监听热流变化
            downstreamFlow.dropWhile {
                //满足条件则丢弃数据
                if (currentDownStreamFlowState is Data<T> ||
                    currentDownStreamFlowState is Final<T>
                ) {
                    //不满足则继续流向map
                    false
                } else {
                    //判断是否满足
                    it === currentDownStreamFlowState
                }
            }.map {
                when (it) {
                    //根据类型,返回不同的值
                    is ReadException<T> -> throw it.readException
                    is Final<T> -> throw it.finalException
                    //正常的返回值
                    is Data<T> -> it.value
                    is 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.coloradmin.cn/o/553913.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

浪涌保护器的不同类型解析

本页提到了不同的电涌保护器类型&#xff0c;即1型电涌保护器&#xff0c;2型&#xff0c;3型和4型电涌保护器。 它提到了电涌保护器类型&#xff0c;即GDT&#xff08;气体放电管&#xff09;&#xff0c;TSPD&#xff08;晶闸管浪涌保护装置&#xff09;&#xff0c;TVS&…

来赞达Lazada商品详情接口(item_get-根据ID取商品详情)代码封装

item_get-根据ID取商品详情接口 通过代码封装该接口可以拿到商品标题&#xff0c;商品价格&#xff0c;商品促销信息&#xff0c;商品优惠价&#xff0c;商品库存&#xff0c;sku属性&#xff0c;商品图片&#xff0c;desc图片&#xff0c;desc描述&#xff0c;sku图片&#xf…

记一次支付宝支付的功能开发

背景&#xff1a; 公司需要增加一项支付宝PC端的收款功能 解决&#xff1a; 使用的支付宝官方文档中的电脑网站支付->统一收单下单并支付&#xff0c;当然&#xff0c;我们的支付宝账号需要开通该产品。官方API连接&#xff1a;https://opendocs.alipay.com/open/028r8t?…

小航助学信息学奥赛C++ GoC期末考试试卷(含题库答题软件账号)

需要在线模拟训练的题库账号请点击 小航助学编程在线模拟试卷系统&#xff08;含题库答题软件账号&#xff09;_程序猿下山的博客-CSDN博客 单选题4.0分 删除编辑 答案:B 第1题GoC的编译运行的快捷键是&#xff08;&#xff09; A、F5B、F11C、F8D、F12 答案解析&#xf…

超声波雷达介绍 ———— 分类介绍

文章目录 介绍安装位置UPAAPA 传感器种类等方性传感器超声波雷达异方性传感器超声波雷达 技术方案模拟式四线式数位二线式数位三线式主动数位 其他密闭式超声波传感器开放式超声波传感器 介绍 超声波的定义 —— 波长短于2cm的机械波称为“超声波”。 超声波属于机械波&#xf…

【prism】容器使用

获取容器对象 当我们使用了prism框架之后,我们就会拥有一个 对象创建/管理 工厂 —— 容器(IOC)。 回忆一下prism工程的构架过程: 我们当前的App 其实 已经继承了 Prism 给我们提供的APP,所以,我们当前的App对象其实就包含了一个容器对象,所以我们可以从任何地方拿到…

javaweb实验:Servlet应用开发

目录 前言实验目的实验内容实验原理或流程图实验过程一个简单的servletServlet表单结果展示 使用servlet处理表单请求servlet结果展示 在Servlet中获取应用程序配置参数servlet 使用Servlet处理用户登录请求Servlet登录表单登陆成功页面登录失败页面结果展示![在这里插入图片描…

调用百度API实现图像风格转换

目录 1、作者介绍2、基本概念2.1 人工智能云服务与百度智能云2.2 图像风格转换 3、调用百度API实现图像风格转换3.1 配置百度智能云平台3.2 环境配置3.3 完整代码实现3.4 效果展示3.5 问题与分析 1、作者介绍 张元帮&#xff0c;男&#xff0c;西安工程大学电子信息学院&#…

ODB 2.4.0 使用延迟指针 lazy_shared_ptr 时遇到的问题

最近在学习使用C下的ORM库——ODB&#xff0c;来抽象对数据库的CURD&#xff0c;由于C的ORM实在是太冷门了&#xff0c;ODB除了官方英语文档&#xff0c;几乎找不到其他好用的资料&#xff0c;所以在使用过程中也是遇到很多疑惑&#xff0c;也解决很多问题。近期遇到的一个源码…

欧拉角,四元数与旋转矩阵

目录 一、欧拉角二、四元数三、旋转矩阵四、Python下欧拉角、四元数和旋转矩阵的相互转换总结 一、欧拉角 对于在三维空间里的一个参考系&#xff0c;任何坐标系的取向&#xff0c;都可以用三个欧拉角(x,y,z)来表现。对于夹角的顺序和标记&#xff0c;夹角的两个轴的指定&…

Baklib分享:做好企业内部知识管理的方法?

企业内部知识管理是一个重要的任务&#xff0c;它涵盖了许多领域&#xff0c;包括知识生成、知识共享、知识保护等。在现代企业中&#xff0c;知识管理被认为是一项战略性的任务&#xff0c;可以为企业带来许多好处&#xff0c;例如提高员工生产力、减少错误和失误、加强员工的…

RabbitMQ 小白教程,从安装到使用

主要内容 AMQP简介 RabbitMQ简介 RabbitMQ原理 Erlang安装 安装RabbitMQ RabbitMQ账户管理 交换器 学习目标 知识点要求AMQP简介掌握RabbmitMQ简介掌握RabbitMQ原理掌握Erlang安装掌握安装RabbitMQ掌握RabbitMQ账户管理掌握交换器掌握 一、 AMQP简介 1 AMQP是什么?…

微信小程序 vue+nodejs高校食堂外卖点餐平台系统rf6md

前端vueelementui, (1) vue引入elementui 1.使用npm安装element-ui npm i element-ui -S 2.在main.js中导入使用vue import element-ui/lib/theme-chalk/index.css //别忘了导入样式 import ElementUI from element-ui Vue.use(ElementUI) 后端&#xff1a;java(springbootss…

蓝牙资讯|智能家居标准Matter 1.1 发布,智能家居产品兼容更丰富

据“CSA 连接标准联盟”官方微信号&#xff0c;Matter 1.1 版本已发布&#xff0c;“1.1 版本带来的更新使设备制造商和开发者上手更容易、产品获取认证更方便&#xff0c;也让产品能更快地交付给用户。该版本还为电池供电设备提供了更大支持&#xff0c;而这类设备涉及多种类型…

VIBRO-METER VM600 MPC4 机械保护卡

4个动态通道和2个转速通道每个动态通道2个处理输出&#xff0c;每个双通道1个处理输出(每个MP C4 2个)&#xff0c;每个转速器通道1个处理输出高度可配置的卡支持机械保护应用所需的所有测量&#xff0c;如相对和/或绝对振动高度集成的卡对(带IOC4T)包括传感器电源、缓冲输出、…

5.2 标准IO:文件的打开、关闭及代码实现

目录 标准IO 文件的打开 标准I/O-fopen-mode参数 ​编辑 标准I/O-fopen-示例 标准I/O-fopen-新建文件权限 标准I/O-处理错误信息 标准I/O-错误信息处理-示例1 标准I/O-错误信息处理-示例2 文件的关闭 标准IO 文件的打开 打开就是占用资源 下列函数可用于打开一个…

四个PCB工程师最头痛的Allegro问题及解答,你一定要看

Allegro是一款功能强大的PCB设计软件&#xff0c;广泛应用在电子设计行业&#xff0c;在使用Allegro过程中&#xff0c;工程师会遇见到多种复杂的技术问题&#xff0c;本文将针对工程师最头痛的Allegro问题进行回答&#xff0c;希望对小伙伴们有所帮助。 1、如何创建新的Allegr…

汽车电子电气架构里的VCU DCU ZCU ECU怎么区分?

文章目录 一、VCU二、DCU三、ECU四、ZCU五、车辆电子电气架构的域架构 一、VCU VCU&#xff08;Vehicle Control Unit&#xff0c;车辆控制单元&#xff09;&#xff1a;VCU是车辆级别的控制单元&#xff0c;负责管理和协调车辆的各个系统和子系统之间的通信和协同工作。它可以…

javaweb实验:JSP+JDBC综合实训__数据库记录的修改、删除

目录 前言实验目的实验内容编程实现后台的用户管理功能 实验原理或流程图实验原理流程图 实验过程AdduserquerAllUserdoAddUser结果展示 实验结论 前言 本实验的目的是让学生掌握使用JSP和JDBC技术对数据库中的记录进行修改和删除的方法。实验内容包括以下几个部分&#xff1a…

STM32单片机蓝牙APP手势语音温控电风扇落地扇人体感应

实践制作DIY- GC0134-蓝牙APP手势语音温控电风扇 一、功能说明&#xff1a; 基于STM32单片机设计-蓝牙APP手势语音温控电风扇 二、功能介绍&#xff1a; 硬件组成&#xff1a;STM32单片机语音识别模块PAJ7620U2手势识别传感器DS18B20温度传感器5V风扇LCD1602显示器红外接…