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

这篇具有很好参考价值的文章主要介绍了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):Boolean
    fun 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): String
    fun <T> deserialize(obj: String, classOfT: Class<T>): T
    fun <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): String
    fun 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.keys
            allKeys.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}"
    }

}

通过上面的代码,我们就可以实现订阅数据的改变。文章来源地址https://www.toymoban.com/news/detail-639006.html

到了这里,关于Android上的基于协程的存储框架的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【Android源码面试宝典】MMKV从使用到原理分析(二)

    上一章节,我们从使用入手,进行了MMKV的简单讲解,我们通过分析简单的运行时日志,从中大概猜到了一些MMKV的代码内部流程,同时,我们也提出了若干的疑问?还是那句话,带着目标(问题)去阅读一篇源码,那么往往收获的知识,更加深入扎实。 本节,我们一起来从源

    2024年01月17日
    浏览(36)
  • golang 协程的实现原理

    要理解协程的实现, 首先需要了解go中的三个非常重要的概念, 它们分别是 G ,  M 和 P , 没有看过golang源代码的可能会对它们感到陌生, 这三项是协程最主要的组成部分, 它们在golang的源代码中无处不在. G (goroutine) G是goroutine的头文字, goroutine可以解释为受管理的轻量线程, gorout

    2024年02月10日
    浏览(44)
  • Unity 的协程的原理

    Unity是一款非常强大的游戏引擎,它支持多种编程语言,其中最常用的语言是C#。在Unity中,协程是一种非常强大的功能,它可以让我们在游戏中实现各种各样的效果。本文将详细介绍Unity协程的原理,并给出示例代码详解。 对啦!这里有个游戏开发交流小组里面聚集了一帮热

    2024年02月02日
    浏览(43)
  • 【Kotlin】协程的字节码原理

    前言 协程是Koltin语言最重要的特性之一,也是最难理解的特性。网上关于kotlin协程的描述也是五花八门,有人说它是轻量级线程,有人说它是无阻塞式挂起,有人说它是一个异步框架等等,众说纷芸。甚至还有人出了书籍专门介绍kotlin协程。 笔者刚开始接触这个概念也是一

    2024年01月18日
    浏览(41)
  • [Kotlin Tutorials 21] 协程的取消

    本文讨论协程的取消, 以及实现时可能会碰到的几个问题. 本文属于合辑: https://github.com/mengdd/KotlinTutorials 取消的意义: 避免资源浪费, 以及多余操作带来的问题. 基本特性: cancel scope的时候会cancel其中的所有child coroutines. 一旦取消一个scope, 你将不能再在其中launch新的coroutine. 一

    2024年02月08日
    浏览(48)
  • Kotlin协程的JVM实现源码分析(上)

    本文从协程的启动 launch 源码入手分析,协程JVM实现分为两篇: 协程启动和执行源码分析 无栈协程 和 Continuation 基本环境: IntelliJ IDEA 2023.3.2 Kotlin 1.8.20 kotlinx-coroutines-core 1.7.3 gradle 8.2 以 GlobalScope.launch 启动协程分析: 调用关系: CoroutineScope.launch - StandaloneCoroutine.start - Corou

    2024年01月19日
    浏览(39)
  • Kotlin协程的JVM实现源码分析(下)

    协程 根据 是否保存切换 调用栈 ,分为: 有栈协程(stackful coroutine) 无栈协程(stackless coroutine) 在代码上的区别是:是否可在普通函数里调用,并暂停其执行。 Kotlin协程,必须在挂起函数中调用和恢复,属于 无栈协程 。 常见的语言,协程实现: 有栈协程 :Go、Lua 无栈

    2024年01月23日
    浏览(45)
  • Kotlin: 协程的四种启动模式(CoroutineStart)

    点击查看CoroutineStart英文文档 创建协程的三种方式 runBlocking 运行一个协程并且会阻塞当前线程,直到它完成。 launch 启动一个新的协程,不会阻塞当前线程,并且返回一个Job,可以取消。 async async和await是两个函数,这两个函数在我们使用过程中一般都是成对出现的。 async用

    2024年04月23日
    浏览(50)
  • Unity中停止协程的多种方式解析

    在Unity3D游戏开发中,协程(Coroutine)是一种非常有用的功能,可以在游戏中实现延迟执行、定期执行和异步操作等任务。然而,有时候我们需要在运行时停止协程的执行。本文将介绍Unity中停止协程的几种常用方式,并提供相应的源代码示例。 使用StopCoroutine函数停止协程

    2024年02月03日
    浏览(70)
  • Unity 之 错误的停止协程的方式

    相信很多人都会这样开启一个协程 这样确实没啥毛病,那么怎么关掉这个协程呢,是不是在想也是一样的传cor_1()这个参数,然后start对应stop,试着输入stopCor....诶,代码提示有这个方法喔,然后写下了这样的代码 结果你会发现这个协程并没有被停下来。。。那该咋办呢?我在

    2024年02月16日
    浏览(39)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包