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

前言

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/22707.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Maven基础学习---5、其他核心概念

1、生命周期 1、作用 为了让构建过程自动化完成&#xff0c;Maven设定了三个生命周期。生命周期中的每一个环节对应构建过程中的一个操作。 2、三个生命周期 3、特点 前面三个生命周期彼此都是独立的在任何一个生命周期内部&#xff0c;执行任何一个具体环节的操作&#xff…

GC 三色标记算法(Go Java版本)

一、前言 GC全称Garbage Collection&#xff0c;目前主流的垃圾回收算法有两类&#xff0c;分别是追踪式垃圾回收算法&#xff08;Tracing garbage collection&#xff09;和引用计数法&#xff08; Reference counting &#xff09;。 而三色标记法是属于追踪式垃圾回收算法…

我出版了一本关于TikTok电商运营的书

回首2020年初&#xff0c;第一次在手机上下载TikTok的那个下午&#xff0c;我并没有意识到&#xff0c;未来三年多这个词会充满我的工作与生活。 那其实是非常幸福的一段时间&#xff0c;对TikTok的期待没有那么功利&#xff0c;每天刷一刷TikTok中的视频&#xff0c;再随手拍…

车辆合格证怎么转为结构化excel数据?

一、为何要将车辆合格证转为结构化excel&#xff1f; 车辆合格证是在车辆制造完成后&#xff0c;经过各项检测合格的证明。对于车辆行业来说&#xff0c;车辆合格证是一种重要的合规证明&#xff0c;在车辆的生产制造、售后服务、质量管理等各个环节中都有着重要的作用。同时&…

git pull报没有足够内存 not enough memory for initialization

git clone 或 git pull 批量同步远程 git仓库代码时&#xff0c;报 没有足够内存用于初始化 not enough memory for initialization。经过观察 资源管理器 的内存使用情况&#xff0c;发现为 剩余可用内存不足造成的。加物理内存麻烦&#xff0c;可通过适当调整 分页文件&…

软考知识点---08IP地址与域名地址

&#x1f4e2;博客主页&#xff1a;盾山狂热粉的博客_CSDN博客-C、C语言,机器视觉领域博主&#x1f4e2;努力努力再努力嗷~~~✨ 一、IP地址 &#xff08;一&#xff09;什么是IP地址&#xff1f; 连入互联网的计算机&#xff0c;每台计算机或者路由器都有一个由授权机构分配的…

煤矿电子封条实施方案 yolov7

煤矿电子封条实施方案采用YOLOv7网络模型算法技术&#xff0c;煤矿电子封条实施算法模型过将全国各省矿山实时监测数据&#xff0c;实现对全国各矿山及时有效的处理及分析。YOLOv7 的发展方向与当前主流的实时目标检测器不同&#xff0c;研究团队希望它能够同时支持移动 GPU 和…

零入门kubernetes网络实战-33->基于nat+brigde+veth pair形成的跨主机的内网通信方案

《零入门kubernetes网络实战》视频专栏地址 https://www.ixigua.com/7193641905282875942 本篇文章视频地址(稍后上传) 本文主要使用的技术是 nat技术Linux虚拟网桥虚拟网络设备veth pair来实现跨主机网桥的通信 1、测试环境介绍 两台centos虚拟机 # 查看操作系统版本 cat …

Unity3D安装:从命令行安装 Unity

推荐&#xff1a;将 NSDT场景编辑器 加入你的3D工具链 3D工具集&#xff1a; NSDT简石数字孪生 从命令行安装 Unity 如果要在组织中自动部署 Unity&#xff0c;可以从命令行安装 Editor 和其他组件。这些组件是普通的安装程序可执行程序和软件包&#xff0c;可以给用来自动部署…

圣墟传说H5手工端搭建架设教程

圣墟传说H5手工端搭建架设教程 大家好&#xff0c;我是艾西。今天给大家带来的游戏是由小说改编而来的大型玄幻MMORPG仙侠手游&#xff0c;也是比较老的游戏了虽然你可能没有怎么听过&#xff0c;但总会有一批喜欢的玩家热衷于它。 那么让我们直接进入正题开始操作&#xff1…

【状态估计】电力系统状态估计的虚假数据注入攻击建模与对策(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

云原生|Kubernetes Operator测试实例

目录 一、主要代码介绍 &#xff08;一&#xff09;变量定义&#xff1a; &#xff08;二&#xff09;测试程序入口 &#xff08;三&#xff09;before函数 &#xff08;四&#xff09;after函数 二、实际测试 &#xff08;一&#xff09;块划分 &#xff08;二&#x…

原神服务端搭建架设Centos系统

原神服务端搭建架设Centos系统 我是艾西&#xff0c;今天为大家带来原神服务端centos系统的教程 Step1. 准备工具 这个端在Windows、Linux系统上都可以跑&#xff0c;本次教程基于Linux。 准备如下工具&#xff1a; 服务器1台 centos7 系统 最低配置32核32G 公网联机 2. 手…

动态规划问题实验:数塔问题

目录 前言实验内容实验流程实验过程实验分析伪代码代码实现分析算法复杂度用例测试 总结 前言 动态规划是一种解决复杂问题的方法&#xff0c;它将一个问题分解为若干个子问题&#xff0c;然后从最简单的子问题开始求解&#xff0c;逐步推导出更复杂的子问题的解&#xff0c;最…

设计原则-单一职责原则

在编程大环境中&#xff0c;评价代码组织方式质量的好坏涉及到各个方面&#xff0c;如代码的可读性、可维护性、可复用性、稳定性等各个方面。而在面向对象语言中也可以通过以下各个方面&#xff1a; 类中方法的设计类中属性的设计类(接口、抽象类、普通类)的设计类与类之间的…

十万条数据,后端不分页咋办!(如何优化长列表渲染)

十万条数据&#xff0c;后端不分页咋办&#xff01;&#xff08;如何优化长列表渲染&#xff09; 长列表是什么&#xff1f; 我们通常把一组数量级很大的数据叫做长列表&#xff0c;比如渲染一组上千条的数据&#xff0c;我们以数组的形式拿到这些信息&#xff0c;然后遍历渲…

正点原子ALPHA开发板核心资源分析

目录 正点原子ALPHA开发板核心资源分析I.MX6ULL实物图对比SOC 主控芯片&#xff08;MCIMX6Y2CVM08AB&#xff09;NAND FLASHEMMCDDR3L 正点原子ALPHA开发板核心资源分析 I.MX6ULL实物图对比 I.MX6ULL NAND BTB 接口核心板资源图与 I.MX6ULL EMMC BTB 接口核心板资源图如上图&a…

电商项目9:新增商品

电商项目9&#xff1a;新增商品 1、前端1.1、修复前端组件通信问题1.2、引入其他前端代码1.3、会员等级列表1.4、当前分类关联的所有品牌 2、后端2.1、会员系统搭建&#xff08;注册与发现&#xff09;2.2、当前分类关联的所有品牌2.3、获取分类下所有分组&关联属性 1、前端…

shell sed命令

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 sed 命令sed 编辑器sed 的工作流程的三个过程命定格式常用选项常用操作 实验操作打印内容使用地址删除行替换插入 sed 命令 sed 编辑器 sed是一种流编辑器&#x…

听我一句劝,别去外包,干了6年,废了....

先说一下自己的情况&#xff0c;大专生&#xff0c;18年通过校招进入湖南某软件公司&#xff0c;干了接近6年的功能测试&#xff0c;今年年初&#xff0c;感觉自己不能够在这样下去了&#xff0c;长时间呆在一个舒适的环境会让一个人堕落!而我已经在一个企业干了6年的功能测试&…