当前位置: 首页 > news >正文

Android上的基于协程的存储框架

在Android上,经常会需要持久化本地数据,比如我们需要缓存用户的配置信息、用户的数据、缓存数据、离线缓存数据等等。我们通常使用的工具为SharePreference、MMKV、DataStore、Room、文件等等。通过使用现有的存储框架,结合协程,我们可以方便地实现一个轻量级的响应式存储框架。

在使用的场景上,我们使用Key-Value的场景很多,而且我们往往不仅仅是存储数据、获取数据,经常还有需要序列化存储、加密存储、订阅数据的变化的功能。

订阅数据的变化,常见的就是使用发布/订阅模式来实现。

但是使用类如EventBus和RxBus并不是一个好的实践,EventBus没有做适当的封装被滥用的话,会导致逻辑混乱,难以跟踪,并且调试起来也相当困难。

谷歌的DataStore就是一个很好的实现。除了DataStore,我们其实也可以使用基于现有的SharePreference、MMKV通过协程等来实现我们的响应式存储框架。

下面我们就来设计这个存储框架。

首先我们基于我们的功能来定义我们的接口

我们的功能如下

  1. 1.支持存储和读取
  2. 2.支持加密和解密
  3. 3.支持序列化和反序列化
  4. 4.支持多“仓库"

由此我们定义了3组接口

  1. 1.Storage 存储器
  2. 2.Serializer 序列化器
  3. 3.CryptoHandler 加密和解密处理器

在清洁架构的分层中,存储(Storage)是属于一种"接口适配器",因为它为应用的内部业务逻辑(即领域层)提供了与外部世界(即数据库、网络、文件系统等)的接口。一般在Respository中和这些接口适配器进行通讯来获取和存储数据,所以在设计Storage的时候,我们应该遵循下面的概念。

Storage接口定义了一个抽象的存储协议,不关注具体的实现方式,例如使用SharedPreferences,MMKV,或者DataStore,这正是适配器层的职责。通过适配器层,我们可以使得业务逻辑从具体的技术细节中解耦,使其更关注于应用的业务规则,而不是底层的存储细节。

同时,我们的设计要允许我们根据需要,灵活地更换或者修改存储的具体实现,而无需改动业务逻辑或者其他部分的代码。

而这正是清洁架构的一个重要原则:独立性和隔离变化,即依赖抽象而不是具体实现。

基于此设计如下的存储器接口

interface Storage {fun put( key:String, obj:Any?)operator fun <T> get( key: String, classOfT:Class<T>):T?operator fun <T> get( key: String, typeOfT: Type):T?fun contain( key: String):Booleanfun onKeyChanged( key:String): Flow<String>fun remove( key: String)fun removeAllPrefix( prefixKey:String )fun removeExcludePrefix( vararg prefixKey: String )fun clear()
}
inline operator fun <reified T> Storage.get(key: String): T? {return get(key, T::class.java)
}

Storage接口设计将基本的存储操作抽象化,并通过onKeyChanged提供了数据变化的通知,这是一个非常有用的功能,使得可以对存储数据的改变进行反应。

此外,removeAllPrefix和removeExcludePrefix方法也为更精细的数据控制提供了可能性,这在处理具有特定前缀键值对的场景中非常有用。

Storage接口设计的目的是为了隐藏实现细节和提高代码的可读性、可维护性和可扩展性。

下面我们基于此继续扩展我们的Storage功能

首先,我们的数据我们希望是序列化存储的,并且可以支持加密。

因此我们继续定义接口:


interface Serializer {fun serialize(obj: Any): Stringfun <T> deserialize(obj: String, classOfT: Class<T>): Tfun <T> deserialize(obj: String, typeOfT: Type): T
}
inline fun <reified T> Serializer.deserialize(obj: String): T = deserialize(obj, T::class.java)

然后是加密和解密接口:

interface CryptoHandler {fun encrypt(obj: String): Stringfun decrypt(obj: String): String
}

接下来我们就可以使用这两个接口来执行序列化、反序列化,加密和解密的操作。

首先MMKV是支持加密的,但是MMKV使用的是AES CFB-128加密算法来做的。但是它并不是那么足够安全,它没有提供硬件级别的安全加密方法。所以可以考虑自己使用Android KeyStore 来实现硬件级别的加密。

使用Android Keystore来实现,一般大致思路就是拿使用Android的keystore 创建一组加密对密钥,然后使用AES算法来加密和解密。

序列化我们可以使用ProtoBuf或者是json来实现

下面简单使用gson来实现我们的序列化存储如下:

@Singleton
open class JsonSerializer(private val gson: Gson) : Serializer {override fun serialize(obj: Any): String {return gson.toJson(obj)}override fun <T> deserialize(obj: String, classOfT: Class<T>): T {return gson.fromJson(obj, classOfT)}override fun <T> deserialize(obj: String, typeOfT: Type): T {return gson.fromJson(obj, typeOfT)}
}

定义好了接口,实现起来就很简单了,只需要在修改key-value的时候,发送一个key被修改的消息到一个flow,对flow的订阅者就可以订阅数据的改变了。

接下来我们基于MMKV和SharePreference来实现这个存储接口

首先我们来使用SharePreference和MMKV来实现这个存储功能
 

class SharePreferenceStorage (private val context: Context,private val storageType: StorageType,private val serializer: Serializer,private val eventLogger: StorageLogger?,private val cryptoHandler: CryptoHandler?):Storage{private val sharedPreferences: SharedPreferences =context.getSharedPreferences(storageType.alias, Context.MODE_PRIVATE)private val keyChangedFlow = MutableSharedFlow<String>(replay = 100)override fun put(key: String, obj: Any?) {obj?.let {data->sharedPreferences.edit().let {editor->editor.putString( key , serializer.serialize( data ).let {cryptoHandler?.encrypt( it )?:it} )editor.apply()keyChangedFlow.tryEmit( key )eventLogger?.trackEvent(StorageSaveEvent( getStorageName(),key, cryptoHandler != null))}}?: run {remove(key)}}override fun <T> get(key: String, classOfT: Class<T>): T? {sharedPreferences.getString( key ,null  )?.let {cryptoHandler?.decrypt( it )?:it}?.let {eventLogger?.trackEvent(StorageLoadEvent( getStorageName(),key, true))serializer.deserialize( it ,classOfT)}?.let {return it}?:run{return null}}override fun <T> get(key: String, typeOfT: Type): T? {val serializeString =   sharedPreferences.getString( key ,null  )?.let {cryptoHandler?.decrypt( it )?:it}return serializeString?.let {serializer.deserialize( it ,typeOfT)}}override fun onKeyChanged(key: String): Flow<String> {return keyChangedFlow.asSharedFlow()}override fun contains(key: String): Boolean {return sharedPreferences.contains( key )}override fun remove(key: String) {if( contains( key ) ){sharedPreferences.edit().let {editor->editor.remove( key )editor.apply()keyChangedFlow.tryEmit( key )eventLogger?.trackEvent(StorageRemoveEvent( getStorageName(),key))}}}override fun removeAllPrefix(prefixKey: String) {sharedPreferences.all?.let {allData->allData.keys.filter { it.startsWith( prefixKey ) }.forEach {remove( it )}}}override fun removeExcludePrefix(vararg prefixKey: String) {sharedPreferences.all?.let {allData->val prefixSet = prefixKey.toSet()val allKeys = allData.keysallKeys.forEach { key ->if (prefixSet.none { key.startsWith(it) }) {remove(key)}}}}override fun clear() {sharedPreferences.edit().let {editor->sharedPreferences.all.keys.forEach {remove( it )}keyChangedFlow.tryEmit( CLEAR_CACHE )eventLogger?.trackEvent(StorageClearEvent( getStorageName()))}}private fun getStorageName():String{return "SharePreference-${storageType.alias}"}}

下面是基于MMKV的实现:

class MMKVStorage constructor(private val storageType: StorageType,private val serializer: Serializer,private val eventLogger: StorageLogger?,private val cryptoHandler: CryptoHandler?): Storage {private val mmkv: MMKV = MMKV.mmkvWithID( storageType.alias, MMKV.MULTI_PROCESS_MODE)private val keyChangedFlow = MutableSharedFlow<String>(replay = 100)private val subscribeKeyList:MutableList<String> = mutableListOf()override fun put(key: String, obj: Any?) {obj?.let {val serializerObj = serializer.serialize( obj ).let {cryptoHandler?.encrypt( it )?:it}mmkv.encode( key,serializerObj)keyChangedFlow.tryEmit(key)eventLogger?.trackEvent(StorageSaveEvent( getStorageName(),key, cryptoHandler != null))} ?: run{remove(key)}}override fun <T> get(key: String, classOfT: Class<T>): T? {return mmkv.decodeString( key )?.let{ jsonString->eventLogger?.trackEvent(StorageLoadEvent( getStorageName(),key, true))serializer.deserialize(jsonString.let {cryptoHandler?.decrypt(it)?:it},classOfT)}}override fun <T> get(key: String, typeOfT: Type): T? {return mmkv.decodeString( key)?.let { jsonString->eventLogger?.trackEvent(StorageLoadEvent( getStorageName(),key, true))serializer.deserialize( jsonString.let {cryptoHandler?.decrypt(it)?:it}, typeOfT)}}override fun onKeyChanged(key: String): Flow<String> {subscribeKeyList.add(key)return keyChangedFlow.asSharedFlow().filter { it == key }}override fun contains(key: String): Boolean {return mmkv.containsKey( key )}override fun remove(key: String) {mmkv.remove(key).apply()eventLogger?.trackEvent(StorageRemoveEvent( getStorageName(),key))keyChangedFlow.tryEmit( key )}override fun removeAllPrefix( prefixKey:String ){val allKeys = mmkv.allKeys()?.clone()?: emptyArray()allKeys.forEach { if( it.contains(prefixKey)) remove(it) }}override fun removeExcludePrefix(vararg prefixKey: String) {val allKeys = mmkv.allKeys()?.clone() ?: emptyArray()val prefixSet = prefixKey.toSet()allKeys.forEach { key ->if (prefixSet.none { key.startsWith(it) }) {remove(key)}}}override fun clear() {mmkv.allKeys()?.forEach {remove(it)}keyChangedFlow.tryEmit( Storage.CLEAR_CACHE )mmkv.clearAll()eventLogger?.trackEvent(StorageClearEvent( getStorageName()))}private fun getStorageName():String {return "mmkv-${storageType.alias}"}}

通过上面的代码,我们就可以实现订阅数据的改变。

相关文章:

Android上的基于协程的存储框架

在Android上&#xff0c;经常会需要持久化本地数据&#xff0c;比如我们需要缓存用户的配置信息、用户的数据、缓存数据、离线缓存数据等等。我们通常使用的工具为SharePreference、MMKV、DataStore、Room、文件等等。通过使用现有的存储框架&#xff0c;结合协程&#xff0c;我…...

虚拟现实与增强现实技术的商业应用

章节一&#xff1a;引言 随着科技的不断发展&#xff0c;虚拟现实&#xff08;Virtual Reality&#xff0c;简称VR&#xff09;与增强现实&#xff08;Augmented Reality&#xff0c;简称AR&#xff09;技术正日益成为商业领域中的重要创新力量。这两种技术为企业带来了前所未…...

每日后端面试5题 第六天

1. Java中有几种类型的流 字符流、字节流 输入流、输出流 节点流、处理流 2 .Spring支持的几种bean的作用域 五种&#xff1a; 1.singleton bean在每个ioc容器中只有一个实例 2.prototype 可以有多个实例 3-5在web环境中才生效 3.request 每次请求才创建bean 4.se…...

LeetCode150道面试经典题-- 两数之和(简单)

1.题目 给定一个整数数组 nums 和一个整数目标值 target&#xff0c;请你在该数组中找出 和为目标值 target 的那 两个 整数&#xff0c;并返回它们的数组下标。 你可以假设每种输入只会对应一个答案。但是&#xff0c;数组中同一个元素在答案里不能重复出现。 你可以按任意…...

转义字符\

转移字符&#xff0c;就是通过字符&#xff0c;来转变原来字符的意思 常见的转义字符&#xff1a; 1、 2 注&#xff1a;" 的作用和他是类似的 3 4、 当打印\a时&#xff0c;电脑会出现一个警告&#xff0c;蜂鸣的声音 5、 阿斯克码表...

什么是DNS欺骗及如何进行DNS欺骗

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、什么是 DNS 欺骗&#xff1f;二、开始1.配置2.Ettercap启动3.操作 总结 前言 我已经离开了一段时间&#xff0c;我现在回来了&#xff0c;我终于在做一个教…...

Android核心开发之——OpenGL

OpenGL是一种用于编程计算机图形的应用程序编程接口&#xff08;API&#xff09;。它提供了一系列函数和方法&#xff0c;用于绘制2D和3D图形&#xff0c;以及进行渲染和图形处理。OpenGL可以跨平台使用&#xff0c;支持各种操作系统和硬件设备。它被广泛应用于游戏开发、虚拟现…...

公共服务领域:西安新小区业主自立业主委员会年底分红83万以及103万事件区块链资金透明监管与投票解决方案的尝试

公共服务领域:西安新小区业主自立业主委员会年底分红83万以及103万事件区块链资金透明监管与投票解决方案的尝试 作者 重庆电子工程职业学院 | 向键雄 杜小敏 前言 本项目想法来源于,西安新小区业主开出物业自立业主委员会年底分红83万以及103万事件,对于此类事件,我们刨…...

ID3 决策树

西瓜数据集D如下: 编号色泽根蒂敲声纹理脐部触感好瓜1青绿蜷缩浊响清晰凹陷硬滑是2乌黑蜷缩沉闷清晰凹陷硬滑是3乌黑蜷缩浊响清晰凹陷硬滑是4青绿蜷缩沉闷清晰凹陷硬滑是5浅白蜷缩浊响清晰凹陷硬滑是6青绿稍蜷浊响清晰稍凹软粘是7乌黑稍蜷浊响稍糊稍凹软粘是8乌黑稍蜷浊响清晰…...

简单线性回归:预测事物间简单关系的利器

文章目录 &#x1f340;简介&#x1f340;什么是简单线性回归&#xff1f;&#x1f340;简单线性回归的应用场景使用步骤&#xff1a;注意事项&#xff1a; &#x1f340;代码演示&#x1f340;结论 &#x1f340;简介 在数据科学领域&#xff0c;线性回归是一种基本而强大的统…...

Vue2-收集表单数据、过滤器、内置指令与自定义指令、Vue生命周期

&#x1f954;&#xff1a;我徒越万重山 千帆过 万木自逢春 更多Vue知识请点击——Vue.js VUE2-Day4 收集表单数据1、不同标签的value属性2、v-model的三个修饰符 过滤器内置指令与自定义指令1、内置指令2、自定义指令定义语法&#xff08;1&#xff09;函数式&#xff08;2&am…...

正则表达式学习详解

正则表达式 正则表达式&#xff08;Regular Expression&#xff09;&#xff0c;通常简称为正则或正则表达式&#xff0c;是一种用于描述字符串模式的工具。它是由一系列字符和特殊字符组成的字符串&#xff0c;用于定义搜索模式或进行字符串匹配、替换、提取等操作。 正则表…...

工具箱:在线免费使用的文档工具:(PDF转换,图片压缩等)

这些都是博主亲自使用过的&#xff0c;可以使用。 PDF转换器&#xff1a; http://www.pdfdo.com/ 图片压缩&#xff1a; 免费在线图片/视频压缩工具 | 图片压缩 | 免费 JPG PNG GIF 图像压缩 (yalijuda.com) 文档OCR转EXCEL: 文字识别 OCR_ 图片文字识别_图片文字智能识别…...

Qt6之QStackedWidget——Qt仿ToDesk(2)

一、 QStackedWidget概述 QStackedWidget也叫堆栈窗体类&#xff0c;它继承于QFrame&#xff0c;主要与QListWidget等结合使用&#xff0c;实现“一个界面多个页面切换”。 二、QStackedWidget示例 如下图&#xff0c;当点击左边 QListWidget里的菜单时&#xff0c;右边跟随切…...

Harbor企业镜像仓库部署(本地)

简述&#xff1a; Docker 官方镜像仓库是用于管理公共镜像的地方&#xff0c;大家可以在上面找到想要的镜像&#xff0c;也可以把自己的镜像推送上去。但是有时候服务器无法访问互联网&#xff0c;或者不希望将自己的镜像放到互联网上&#xff0c;那么就需要用到 Docker Regis…...

【Linux】如何打包成动静态库,第三方动静态库如何使用?

文章目录 1. 打包成静态库2. 打包成动态库&#xff08;共享库&#xff09;3. 使用第三方静态库4. 使用第三方动态库 5. 动态库的加载6. 注意事项 库的名称&#xff1a;去掉前面的 lib 去掉后面的 .a(版本号) .so(版本号) 剩下的&#xff0c;才是库正真的名称。 查看文件依赖库…...

SAP MM学习笔记20- SAP中的英文2 - SD中英文,日语,中文

SD模块中的英文&#xff0c;日语&#xff0c;中文 对照。 販売管理 日本語英語中国語受注伝票sales order销售订单出荷伝票delivery order交货订单ピッキングリストpicking list领货清单シップメント伝票shipment document发运单据出庫確認post goods issue发货确认請求伝票b…...

计算机网络中的一些基本概念

IP地址: 址用于定位主机的网络地址。是一个32位的二进制数&#xff0c;通常被分割为4个“8位二进制数”&#xff08;也就是4个字节&#xff09;.**端口号:**在网络通信中&#xff0c;IP地址用于标识主机网络地址&#xff0c;端口号可以标识主机中发送数据、接收数据的进程。简单…...

pytest 用例运行方式

一、命令行方式运行 执行某个目录下所有的用例&#xff0c;符合规范的所有用例 进入到对应的目录,直接执行pytest; 例如需要执行testcases 下的所有用例; 可以进入testcases 目录; 然后执行pytest 进入对应目录的上级目录,执行pytest 目录名称/ ; ; 例如需要执行testcases 下…...

简单入门seleniumUI自动化测试

目录 一、selenium的介绍 二、selenium的原理 三、selenium的八种元素定位的方法 1、ID定位&#xff1a; 2 、name定位&#xff1a; 3、class定位&#xff1a; 4、tag定位&#xff1a; 5、link_text定位&#xff1a; 6、partial_link_text定位&#xff1a; 7、css定位…...

线程与协程

1. 线程与协程 1.1. “函数调用级别”的切换、上下文切换 1. 函数调用级别的切换 “函数调用级别的切换”是指&#xff1a;像函数调用/返回一样轻量地完成任务切换。 举例说明&#xff1a; 当你在程序中写一个函数调用&#xff1a; funcA() 然后 funcA 执行完后返回&…...

visual studio 2022更改主题为深色

visual studio 2022更改主题为深色 点击visual studio 上方的 工具-> 选项 在选项窗口中&#xff0c;选择 环境 -> 常规 &#xff0c;将其中的颜色主题改成深色 点击确定&#xff0c;更改完成...

学校招生小程序源码介绍

基于ThinkPHPFastAdminUniApp开发的学校招生小程序源码&#xff0c;专为学校招生场景量身打造&#xff0c;功能实用且操作便捷。 从技术架构来看&#xff0c;ThinkPHP提供稳定可靠的后台服务&#xff0c;FastAdmin加速开发流程&#xff0c;UniApp则保障小程序在多端有良好的兼…...

c++ 面试题(1)-----深度优先搜索(DFS)实现

操作系统&#xff1a;ubuntu22.04 IDE:Visual Studio Code 编程语言&#xff1a;C11 题目描述 地上有一个 m 行 n 列的方格&#xff0c;从坐标 [0,0] 起始。一个机器人可以从某一格移动到上下左右四个格子&#xff0c;但不能进入行坐标和列坐标的数位之和大于 k 的格子。 例…...

leetcodeSQL解题:3564. 季节性销售分析

leetcodeSQL解题&#xff1a;3564. 季节性销售分析 题目&#xff1a; 表&#xff1a;sales ---------------------- | Column Name | Type | ---------------------- | sale_id | int | | product_id | int | | sale_date | date | | quantity | int | | price | decimal | -…...

蓝桥杯3498 01串的熵

问题描述 对于一个长度为 23333333的 01 串, 如果其信息熵为 11625907.5798&#xff0c; 且 0 出现次数比 1 少, 那么这个 01 串中 0 出现了多少次? #include<iostream> #include<cmath> using namespace std;int n 23333333;int main() {//枚举 0 出现的次数//因…...

React---day11

14.4 react-redux第三方库 提供connect、thunk之类的函数 以获取一个banner数据为例子 store&#xff1a; 我们在使用异步的时候理应是要使用中间件的&#xff0c;但是configureStore 已经自动集成了 redux-thunk&#xff0c;注意action里面要返回函数 import { configureS…...

Golang——7、包与接口详解

包与接口详解 1、Golang包详解1.1、Golang中包的定义和介绍1.2、Golang包管理工具go mod1.3、Golang中自定义包1.4、Golang中使用第三包1.5、init函数 2、接口详解2.1、接口的定义2.2、空接口2.3、类型断言2.4、结构体值接收者和指针接收者实现接口的区别2.5、一个结构体实现多…...

【Elasticsearch】Elasticsearch 在大数据生态圈的地位 实践经验

Elasticsearch 在大数据生态圈的地位 & 实践经验 1.Elasticsearch 的优势1.1 Elasticsearch 解决的核心问题1.1.1 传统方案的短板1.1.2 Elasticsearch 的解决方案 1.2 与大数据组件的对比优势1.3 关键优势技术支撑1.4 Elasticsearch 的竞品1.4.1 全文搜索领域1.4.2 日志分析…...

DeepSeek越强,Kimi越慌?

被DeepSeek吊打的Kimi&#xff0c;还有多少人在用&#xff1f; 去年&#xff0c;月之暗面创始人杨植麟别提有多风光了。90后清华学霸&#xff0c;国产大模型六小虎之一&#xff0c;手握十几亿美金的融资。旗下的AI助手Kimi烧钱如流水&#xff0c;单月光是投流就花费2个亿。 疯…...