Android上的基于协程的存储框架
在Android上,经常会需要持久化本地数据,比如我们需要缓存用户的配置信息、用户的数据、缓存数据、离线缓存数据等等。我们通常使用的工具为SharePreference、MMKV、DataStore、Room、文件等等。通过使用现有的存储框架,结合协程,我们可以方便地实现一个轻量级的响应式存储框架。
在使用的场景上,我们使用Key-Value的场景很多,而且我们往往不仅仅是存储数据、获取数据,经常还有需要序列化存储、加密存储、订阅数据的变化的功能。
订阅数据的变化,常见的就是使用发布/订阅模式来实现。
但是使用类如EventBus和RxBus并不是一个好的实践,EventBus没有做适当的封装被滥用的话,会导致逻辑混乱,难以跟踪,并且调试起来也相当困难。
谷歌的DataStore就是一个很好的实现。除了DataStore,我们其实也可以使用基于现有的SharePreference、MMKV通过协程等来实现我们的响应式存储框架。
下面我们就来设计这个存储框架。
首先我们基于我们的功能来定义我们的接口
我们的功能如下
- 1.支持存储和读取
- 2.支持加密和解密
- 3.支持序列化和反序列化
- 4.支持多“仓库"
由此我们定义了3组接口
- 1.Storage 存储器
- 2.Serializer 序列化器
- 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上,经常会需要持久化本地数据,比如我们需要缓存用户的配置信息、用户的数据、缓存数据、离线缓存数据等等。我们通常使用的工具为SharePreference、MMKV、DataStore、Room、文件等等。通过使用现有的存储框架,结合协程,我…...
虚拟现实与增强现实技术的商业应用
章节一:引言 随着科技的不断发展,虚拟现实(Virtual Reality,简称VR)与增强现实(Augmented Reality,简称AR)技术正日益成为商业领域中的重要创新力量。这两种技术为企业带来了前所未…...
每日后端面试5题 第六天
1. Java中有几种类型的流 字符流、字节流 输入流、输出流 节点流、处理流 2 .Spring支持的几种bean的作用域 五种: 1.singleton bean在每个ioc容器中只有一个实例 2.prototype 可以有多个实例 3-5在web环境中才生效 3.request 每次请求才创建bean 4.se…...
LeetCode150道面试经典题-- 两数之和(简单)
1.题目 给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。 你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。 你可以按任意…...
转义字符\
转移字符,就是通过字符,来转变原来字符的意思 常见的转义字符: 1、 2 注:" 的作用和他是类似的 3 4、 当打印\a时,电脑会出现一个警告,蜂鸣的声音 5、 阿斯克码表...
什么是DNS欺骗及如何进行DNS欺骗
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言一、什么是 DNS 欺骗?二、开始1.配置2.Ettercap启动3.操作 总结 前言 我已经离开了一段时间,我现在回来了,我终于在做一个教…...
Android核心开发之——OpenGL
OpenGL是一种用于编程计算机图形的应用程序编程接口(API)。它提供了一系列函数和方法,用于绘制2D和3D图形,以及进行渲染和图形处理。OpenGL可以跨平台使用,支持各种操作系统和硬件设备。它被广泛应用于游戏开发、虚拟现…...
公共服务领域:西安新小区业主自立业主委员会年底分红83万以及103万事件区块链资金透明监管与投票解决方案的尝试
公共服务领域:西安新小区业主自立业主委员会年底分红83万以及103万事件区块链资金透明监管与投票解决方案的尝试 作者 重庆电子工程职业学院 | 向键雄 杜小敏 前言 本项目想法来源于,西安新小区业主开出物业自立业主委员会年底分红83万以及103万事件,对于此类事件,我们刨…...
ID3 决策树
西瓜数据集D如下: 编号色泽根蒂敲声纹理脐部触感好瓜1青绿蜷缩浊响清晰凹陷硬滑是2乌黑蜷缩沉闷清晰凹陷硬滑是3乌黑蜷缩浊响清晰凹陷硬滑是4青绿蜷缩沉闷清晰凹陷硬滑是5浅白蜷缩浊响清晰凹陷硬滑是6青绿稍蜷浊响清晰稍凹软粘是7乌黑稍蜷浊响稍糊稍凹软粘是8乌黑稍蜷浊响清晰…...
简单线性回归:预测事物间简单关系的利器
文章目录 🍀简介🍀什么是简单线性回归?🍀简单线性回归的应用场景使用步骤:注意事项: 🍀代码演示🍀结论 🍀简介 在数据科学领域,线性回归是一种基本而强大的统…...
Vue2-收集表单数据、过滤器、内置指令与自定义指令、Vue生命周期
🥔:我徒越万重山 千帆过 万木自逢春 更多Vue知识请点击——Vue.js VUE2-Day4 收集表单数据1、不同标签的value属性2、v-model的三个修饰符 过滤器内置指令与自定义指令1、内置指令2、自定义指令定义语法(1)函数式(2&am…...
正则表达式学习详解
正则表达式 正则表达式(Regular Expression),通常简称为正则或正则表达式,是一种用于描述字符串模式的工具。它是由一系列字符和特殊字符组成的字符串,用于定义搜索模式或进行字符串匹配、替换、提取等操作。 正则表…...
工具箱:在线免费使用的文档工具:(PDF转换,图片压缩等)
这些都是博主亲自使用过的,可以使用。 PDF转换器: http://www.pdfdo.com/ 图片压缩: 免费在线图片/视频压缩工具 | 图片压缩 | 免费 JPG PNG GIF 图像压缩 (yalijuda.com) 文档OCR转EXCEL: 文字识别 OCR_ 图片文字识别_图片文字智能识别…...
Qt6之QStackedWidget——Qt仿ToDesk(2)
一、 QStackedWidget概述 QStackedWidget也叫堆栈窗体类,它继承于QFrame,主要与QListWidget等结合使用,实现“一个界面多个页面切换”。 二、QStackedWidget示例 如下图,当点击左边 QListWidget里的菜单时,右边跟随切…...
Harbor企业镜像仓库部署(本地)
简述: Docker 官方镜像仓库是用于管理公共镜像的地方,大家可以在上面找到想要的镜像,也可以把自己的镜像推送上去。但是有时候服务器无法访问互联网,或者不希望将自己的镜像放到互联网上,那么就需要用到 Docker Regis…...
【Linux】如何打包成动静态库,第三方动静态库如何使用?
文章目录 1. 打包成静态库2. 打包成动态库(共享库)3. 使用第三方静态库4. 使用第三方动态库 5. 动态库的加载6. 注意事项 库的名称:去掉前面的 lib 去掉后面的 .a(版本号) .so(版本号) 剩下的,才是库正真的名称。 查看文件依赖库…...
SAP MM学习笔记20- SAP中的英文2 - SD中英文,日语,中文
SD模块中的英文,日语,中文 对照。 販売管理 日本語英語中国語受注伝票sales order销售订单出荷伝票delivery order交货订单ピッキングリストpicking list领货清单シップメント伝票shipment document发运单据出庫確認post goods issue发货确认請求伝票b…...
计算机网络中的一些基本概念
IP地址: 址用于定位主机的网络地址。是一个32位的二进制数,通常被分割为4个“8位二进制数”(也就是4个字节).**端口号:**在网络通信中,IP地址用于标识主机网络地址,端口号可以标识主机中发送数据、接收数据的进程。简单…...
pytest 用例运行方式
一、命令行方式运行 执行某个目录下所有的用例,符合规范的所有用例 进入到对应的目录,直接执行pytest; 例如需要执行testcases 下的所有用例; 可以进入testcases 目录; 然后执行pytest 进入对应目录的上级目录,执行pytest 目录名称/ ; ; 例如需要执行testcases 下…...
简单入门seleniumUI自动化测试
目录 一、selenium的介绍 二、selenium的原理 三、selenium的八种元素定位的方法 1、ID定位: 2 、name定位: 3、class定位: 4、tag定位: 5、link_text定位: 6、partial_link_text定位: 7、css定位…...
从光谱分析到过程监控:偏最小二乘(PLS)在工业领域的实战避坑指南
从光谱分析到过程监控:偏最小二乘(PLS)在工业领域的实战避坑指南 在制药厂的质量控制实验室里,近红外光谱仪正快速扫描着流水线上的药片。数百个波长数据在屏幕上闪烁,而工程师需要从中准确预测活性成分含量——这正是偏最小二乘回归(PLS)大显…...
终极指南:如何解锁艾尔登法环帧率限制并实现超宽屏支持
终极指南:如何解锁艾尔登法环帧率限制并实现超宽屏支持 【免费下载链接】EldenRingFpsUnlockAndMore A small utility to remove frame rate limit, change FOV, add widescreen support and more for Elden Ring 项目地址: https://gitcode.com/gh_mirrors/el/El…...
【电子技术综合设计】从零构建多功能数字钟:12/24进制切换与闹钟模块的硬件实现
1. 从零开始:数字钟的设计思路与核心功能 第一次接触数字钟设计的朋友可能会觉得这是个复杂的工程,但拆解后其实可以分成几个关键模块。我自己在大学电子设计课上完成这个项目时,也是从最基础的秒计数器开始一步步搭建的。这个数字钟最核心的…...
别再让IRF分裂搞瘫网络!手把手教你配置H3C BFD MAD检测(附排错命令)
H3C IRF分裂应急指南:BFD MAD检测配置与深度排错实战 凌晨三点,数据中心告警声骤然响起——核心交换机的IRF链路突然中断,网络中出现两台"一模一样"的交换机,IP地址冲突、路由表震荡、业务开始大面积瘫痪。这不是演习&a…...
【稀缺预警】全球首份AGI审计胜任力白皮书(2024Q3修订版):覆盖11类高风险会计判断,含FASB ASC 842租赁准则专项验证矩阵
第一章:AGI的财务分析与审计能力 2026奇点智能技术大会(https://ml-summit.org) 通用人工智能(AGI)在财务分析与审计领域已展现出超越传统规则引擎与统计模型的能力。它不仅能实时解析多源异构财务数据(如ERP日志、银行流水、电子…...
避坑指南:STM32 DAC输出缓冲到底开不开?实测对比0V精度与驱动能力
STM32 DAC输出缓冲配置实战:精度与驱动能力的深度权衡 在嵌入式系统设计中,数字模拟转换器(DAC)的性能直接影响着模拟信号输出的质量。许多工程师在使用STM32的DAC功能时,往往忽略了一个关键配置项——输出缓冲&#x…...
CVAT在Ubuntu 20.04上的完整安装指南:从Docker配置到多人协作避坑
CVAT在Ubuntu 20.04上的完整安装指南:从Docker配置到多人协作避坑 在计算机视觉项目中,高质量的数据标注是模型成功的关键。CVAT(Computer Vision Annotation Tool)作为英特尔开源的图像标注工具,凭借其丰富的标注功能…...
别再死记硬背ARP了!用Wireshark抓包,5分钟带你亲眼看看局域网‘喊话’全过程
用Wireshark解密ARP协议:从抓包实战看局域网如何"喊话" 当你第一次听说ARP协议时,是否也被那些"广播请求"、"单播响应"的抽象概念搞得一头雾水?作为网络通信的基础协议之一,ARP(地址解析…...
终极指南:3小时完成100个NCBI基因组数据批量下载的完整解决方案
终极指南:3小时完成100个NCBI基因组数据批量下载的完整解决方案 【免费下载链接】ncbi-genome-download Scripts to download genomes from the NCBI FTP servers 项目地址: https://gitcode.com/gh_mirrors/nc/ncbi-genome-download 作为生物信息学研究人员…...
Tool之Jira:从零到一,构建高效敏捷团队的Jira实战配置与核心流程详解
1. 为什么你的团队需要Jira? 第一次接触Jira的团队常会问:为什么不用Excel或Trello?五年前我带创业团队时也这么想,直到一次版本发布前,测试组长凌晨三点打电话问我:"那个优先级为高的Bug到底分给谁了…...
