通用缓存存储设计方案

通用缓存存储设计方案

目录介绍

  • 01.全体概述阐明
    • 1.1 项目背景介绍
    • 1.2 遇到问题记载
    • 1.3 根底概念介绍
    • 1.4 规划方针
    • 1.5 发生收益剖析
  • 02.市道存储计划
    • 2.1 缓存存储有哪些
    • 2.2 缓存战略有哪些
    • 2.3 常见存储计划
    • 2.4 市道存储计划阐明
    • 2.5 存储计划的缺乏
  • 03.存储计划原理
    • 3.1 Sp存储原理剖析
    • 3.2 MMKV存储原理剖析
    • 3.3 LruCache考量剖析
    • 3.4 DiskLru原理剖析
    • 3.5 DataStore剖析
    • 3.6 HashMap存储剖析
    • 3.7 Sqlite存储剖析
    • 3.8 运用存储的留意点
    • 3.9 各种数据存储文件
  • 04.通用缓存计划思路
    • 4.1 怎么兼容不同缓存
    • 4.2 打造通用缓存Api
    • 4.3 切换不同缓存办法
    • 4.4 缓存的过期处理
    • 4.5 缓存的阀值处理
    • 4.6 缓存的线程安全性
    • 4.7 缓存数据的搬迁
    • 4.8 缓存数据加密处理
    • 4.9 缓存功率的比照
  • 05.计划根底规划
    • 5.1 全体架构图
    • 5.2 UML规划图
    • 5.3 要害流程图
    • 5.4 模块间依靠联系
  • 06.其他规划阐明
    • 6.1 功用规划阐明
    • 6.2 安稳性规划
    • 6.3 灰度规划
    • 6.4 降级规划
    • 6.5 反常规划阐明
    • 6.6 兼容性规划
    • 6.7 自测性规划
  • 07.通用Api规划
    • 7.1 怎么依靠该库
    • 7.2 初始化缓存库
    • 7.3 切换各种缓存计划
    • 7.4 数据的存和取
    • 7.5 线程安全考量
    • 7.6 检查缓存文件数据
    • 7.7 怎么挑选适宜计划
  • 08.其他阐明介绍
    • 8.1 遇到的坑剖析
    • 8.2 留传的问题
    • 8.3 未来的规划
    • 8.4 参阅链接记载

01.全体概述阐明

1.1 项目背景介绍

  • 项目中许多当地运用缓存计划
    • 有的用sp,有的用mmkv,有的用lru,有的用DataStore,有的用sqlite,怎么打造通用api切换操作不同存储计划?
  • 缓存计划众多,且各自运用场景有差异,怎么挑选适宜的缓存办法?
    • 针对不同场景挑选什么缓存办法,一起考虑怎么替换之前老的存储计划,而不用花费很大的时刻成本!
  • 针对不同的事务场景,不同的缓存计划。打造一套通用的计划
    • 屏蔽各种缓存办法的差异性,暴露给外部开发者一致的API,外部开发者简化运用,进步开发功率和运用功率……
    • 通用缓存存储设计方案

1.2 遇到问题记载

  • 记载几个常见的问题
    • 问题1:各种缓存计划,别离是怎么确保数据安全的,其内部运用到了哪些锁?由于引进锁,给功率上带来了什么影响?
    • 问题2:各种缓存计划,进程不安全是否会导致数据丢掉,怎么处理数据丢掉状况?怎么处理脏数据,其原理大概是什么?
    • 问题3:各种缓存计划运用场景是什么?有什么缺点,为了处理缺点做了些什么?比方sp存在缺点的代替计划是DataStore,为何这样?
    • 问题4:各种缓存计划,他们的缓存功率是怎样的?怎么比照?接入该库后,怎么做数据搬迁,怎么掩盖操作?
  • 考虑一个K-V结构的规划
    • 问题1-线程安全:运用K-V存储一般会在多线程环境中履行,因而结构有必要确保多线程并发安全,并且优化并发功率;
    • 问题2-内存缓存:由于磁盘 IO 操作是耗时操作,因而结构有必要在事务层和磁盘文件之间增加一层内存缓存;
    • 问题3-事务:由于磁盘 IO 操作是耗时操作,因而结构有必要将支撑屡次磁盘 IO 操作聚合为一次磁盘写回事务,削减拜访磁盘次数;
    • 问题4-事务串行化:由于程序或许由多个线程主张写回事务,因而结构有必要确保事务之间的事务串行化,避免先履行的事务掩盖后履行的事务;
    • 问题5-异步或同步写回:由于磁盘 IO 是耗时操作,因而结构有必要支撑后台线程异步写回;有时分又要求数据读写是同步的;
    • 问题6-增量更新:由于磁盘文件内容或许很大,因而修正 K-V 时有必要支撑部分修正,而不是全量掩盖修正;
    • 问题7-改变回调:由于事务层或许有监听 K-V 改变的需求,因而结构有必要支撑改变回调监听,并且避免出现内存泄漏
    • 问题8-多进程:由于程序或许有多进程需求,那么结构怎么确保多进程数据同步?
    • 问题9-可用性:由于程序运转中存在不可控的反常和 Crash,因而结构有必要尽或许确保体系可用性,尽量确保体系在遇到反常后的数据完整性;
    • 问题10-高效性:功用永远是要考虑的问题,解析、读取、写入和序列化的功用怎么进步和权衡
    • 问题11-安全性:假如程序需求存储敏感数据,怎么确保数据完整性和保密性;
    • 问题12-数据搬迁:假如项目中存在旧结构,怎么将数据从旧结构搬迁至新结构,并且确保可靠性;
    • 问题13-研发体验:是否模板代码冗长,是否简略犯错。各种K—V结构运用体验怎么?
  • 常见存储结构规划考虑导图
    • 通用缓存存储设计方案

1.3 根底概念介绍

  • 最初缓存的概念
    • 提及缓存,或许很简略想到Http的缓存机制,LruCache,其实缓存最初是针关于网络而言的,也是狭义上的缓存,广义的缓存是指对数据的复用。
  • 缓存容量,便是缓存的巨细
    • 每一种缓存,总会有一个最大的容量,抵达这个极限今后,那么就须要进行缓存整理了结构。这个时分就需求删除一些旧的缓存并增加新的缓存。

1.4 规划方针

  • 打造通用存储库:
    • 规划一个缓存通用计划,其次,它的结构需求很简略,由于许多当地需求用到,再次,它得线程安全。灵敏切换不同的缓存办法,运用简略。
  • 内部开源该库:
    • 作为技能沉积,当作专项来推进发展。高复用低耦合,便于拓宽,可快速移植,处理各个项目运用内存缓存,sp,mmkv,sql,lru,DataStore的杂乱。笼一致套一致的API接口

1.5 发生收益剖析

  • 一致缓存API兼容不同存储计划
    • 打造通用api,抹平了sp,mmkv,sql,lru,dataStore等各种计划的差异性。简化开发者运用,功用强大而运用简略!
    • 通用缓存存储设计方案

02.市道存储计划

2.1 缓存存储有哪些

  • 比较常见的是内存缓存以及磁盘缓存。
    • 内存缓存:这儿的内存首要指的存储器缓存;磁盘缓存:这儿首要指的是外部存储器,手机的话指的便是存储卡。
  • 内存缓存:
    • 经过预先消耗运用的一点内存来存储数据,便可快速的为运用中的组件供给数据,是一种典型的以空间换时刻的战略。
  • 磁盘缓存:
    • 读取磁盘文件要比直接从内存缓存中读取要慢一些,并且需求在一个UI主线程外的线程中进行,由于磁盘的读取速度是不能够确保的,磁盘文件缓存明显也是一种以空间换时刻的战略。
  • 二级缓存:
    • 内存缓存和磁盘缓存结合。比方,LruCache将图片保存在内存,存取速度较快,退出APP后缓存会失效;而DiskLruCache将图片保存在磁盘中,下次进入运用后缓存仍旧存在,它的存取速度比较LruCache会慢上一些。

2.2 缓存战略有哪些

  • 缓存的中心思维首要是什么呢
    • 一般来说,缓存中心步骤首要包含缓存的增加、获取和删除这三类操作。那么为什么还要删除缓存呢?不论是内存缓存仍是硬盘缓存,它们的缓存巨细都是有限的。
    • 当缓存满了之后,再想其增加缓存,这个时分就需求删除一些旧的缓存并增加新的缓存。这个跟线程池满了今后的线程处理战略类似!
  • 缓存的常见的战略有那些
    • FIFO(first in first out):先进先出战略,类似行列。
    • LFU(less frequently used):最少运用战略,RecyclerView的缓存选用了此战略。
    • LRU(least recently used):最近最少运用战略,Glide在进行内存缓存的时分选用了此战略。

2.3 常见存储计划

  • 内存缓存:存储在内存中,假如方针毁掉则内存也会跟随毁掉。假如是静态方针,那么进程杀身后内存会毁掉。
    • Map,LruCache等等
  • 磁盘缓存:后台运用有或许会被杀死,那么相应的内存缓存方针也会被毁掉。当你的运用重新回到前台显示时,你需求用到缓存数据时,这个时分能够用磁盘缓存。
    • SharedPreferences,MMKV,DiskLruCache,SqlLite,DataStore,Room,Realm,GreenDao等等

2.4 市道存储计划阐明

  • 内存缓存
    • Map:内存缓存,一般用HashMap存储一些数据,首要存储一些暂时的方针
    • LruCache:内存筛选缓存,内部运用LinkedHashMap,会筛选最长时刻未运用的方针
  • 磁盘缓存
    • SharedPreferences:轻量级磁盘存储,一般存储装备特点,线程安全。主张不要存储大数据,不支撑跨进程!
    • MMKV:腾讯开源存储库,内部选用mmap。
    • DiskLruCache:磁盘筛选缓存,写入数据到file文件
    • SqlLite:移动端轻量级数据库。首要是用来方针耐久化存储。
    • DataStore:旨在代替原有的 SharedPreferences,支撑SharedPreferences数据的搬迁
    • Room/Realm/GreenDao:支撑大型或杂乱数据集
    • 通用缓存存储设计方案
  • 其他开源缓存库
    • ACache:一款高效二级存储库,选用内存缓存和磁盘缓存

2.5 存储计划的缺乏

  • 存储计划SharedPreferences的缺乏
    • 1.SP用内存层用HashMap保存,磁盘层则是用的XML文件保存。每次更改,都需求将整个HashMap序列化为XML格局的报文然后整个写入文件。
    • 2.SP读写文件不是类型安全的,且没有发犯过错信号的机制,缺少事务性API
    • 3.commit() / apply()操作或许会形成ANR问题
  • 存储计划MMKV的缺乏
    • 1.没有类型信息,不支撑getAll。由于没有记载类型信息,MMKV无法主动反序列化,也就无法完结getAll接口。
    • 2.需求引进so,增加包体积:引进MMKV需求增加的体积仍是不少的。
    • 3.文件只增不减:MMKV的扩容战略仍是比较急进的,并且扩容之后不会主动trim size。
  • 存储计划DataStore的缺乏
    • 1.只是供给异步API,没有供给同步API办法。在进行许多同步存储的时分,运用runBlocking同步数据或许会卡顿。
    • 2.对主线程履行同步 I/O 操作或许会导致 ANR 或界面卡顿。能够经过从 DataStore 异步预加载数据来削减这些问题。

03.存储计划原理

3.1 Sp存储原理剖析

  • SharedPreferences,它是一个轻量级的存储类,特别适宜用于保存软件装备参数。
    • 轻量级,以键值对的办法进行存储。选用的是xml文件办法存储在本地,程序卸载后会也会同时被铲除,不会残留信息。线程安全的。
  • 它有一些坏处如下所示
    • 对文件IO读取,因而在IO上的瓶颈是个大问题,由于在每次进行get和commit时都要将数据从内存写入到文件中,或从文件中读取。
    • 多线程场景下功率较低,在get操作时,会确定SharedPreferences方针,互斥其他操作,而当put,commit时,则会确定Editor方针,运用写入锁进行互斥,在这种状况下,功率会下降。
    • 不支撑跨进程通讯,由于每次都会把整个文件加载到内存中,不主张存储大的文件内容,比方大json。
  • 有一些运用上的主张如下
    • 主张不要存储较大数据;频频修正的数据修正后一致提交而不是修正过后立刻提交;在跨进程通讯中不去运用;键值对不宜过多
  • 读写操作功用剖析
    • 第一次经过Context.getSharedPreferences()进行初始化时,对xml文件进行一次读取,并将文件内一切内容(即一切的键值对)缓到内存的一个Map中,接下来一切的读操作,只需求从这个Map中取就能够

3.2 MMKV存储原理剖析

  • 前期微信的需求
    • 微信谈天对话内容中的特别字符所导致的程序溃散是一类很常见、也很需求快速处理的问题;而哪些字符会导致程序溃散,是无法预知的。
    • 只能等用户手机上的微信溃散之后,再利用类似时光倒流的回溯行为,看看上次软件溃散的终究一会儿,用户收到或许宣布了什么音讯,再用这些音讯中的文字去尝试复现发生过的溃散,终究试出有问题的字符,然后针对性处理。
  • 该需求对应的技能考量
    • 考量1:把谈天页面的显示文字写到手机磁盘里,才干在程序溃散、重新启动之后,经过读取文件的办法来检查。但这种办法涉及到io流读写,且音讯多会有功用问题。
    • 考量2:App程序都溃散了,怎么确保要存储的内容,都写入到磁盘中呢?
    • 考量3:保存谈天内容到磁盘的行为,这个做成同步仍是异步呢?假如是异步,怎么确保谈天音讯的时序性?
    • 考量4:怎么存储数据是同步行为,针对群里谈天这么多音讯,怎么才干避免卡顿呢?
    • 考量5:存储数据放到主线程中,用户在群谈天页面猛滑音讯,怎么爆发性集中式对磁盘写入数据?
  • MMKV存储结构介绍
    • MMKV 是根据 mmap 内存映射的 key-value 组件,底层序列化/反序列化运用 protobuf 完结,功用高,安稳性强。
  • MMKV规划的原理
    • 内存预备:经过 mmap 内存映射文件,供给一段可供随时写入的内存块,App 只管往里边写数据,由操作体系担任将内存回写到文件,不用担心 crash 导致数据丢掉。
    • 数据安排:数据序列化方面我们选用 protobuf 协议,pb 在功用和空间占用上都有不错的体现。
    • 写入优化:考虑到首要运用场景是频频地进行写入更新,需求有增量更新的能力。考虑将增量 kv 方针序列化后,append 到内存结尾。
    • 空间增加:运用 append 完结增量更新带来了一个新的问题,便是不断 append 的话,文件巨细会增加得不可控。需求在功用和空间上做个折中。
  • MMKV诞生的背景
    • 针对该事务,高频率,同步,许多数据写入磁盘的需求。不论用sp,仍是store,仍是disk,仍是数据库,只要在主线程同步写入磁盘,会很卡。
    • 处理计划便是:运用内存映射mmap的底层办法,相当于体系为指定文件开辟专用内存空间,内存数据的改动会主动同步到文件里。
    • 用浅显的话说:MMKV便是完结用「写入内存」的办法来完结「写入磁盘」的方针。内存的速度多快呀,耗时几乎能够疏忽,这样就把写磁盘形成卡顿的问题处理了

3.3 LruCache考量剖析

  • 在LruCache的源码中,关于LruCache有这样的一段介绍:

    • cache方针经过一个强引证来拜访内容。每次当一个item被拜访到的时分,这个item就会被移动到一个行列的队首。当一个item被增加到已经满了的行列时,这个行列的队尾的item就会被移除。
  • LruCache中心思维

    • LRU是近期最少运用的算法,它的中心思维是当缓存满时,会优先筛选那些近期最少运用的缓存方针。选用LRU算法的缓存有两种:LrhCache和DiskLruCache,别离用于完结内存缓存和硬盘缓存,其中心思维都是LRU缓存算法。
    • 通用缓存存储设计方案
  • LruCache运用是计数or计量

    • 运用计数战略:1、Message 音讯方针池:最多缓存 50 个方针;2、OkHttp 衔接池:默许最多缓存 5 个空闲衔接;3、数据库衔接池
    • 运用计量战略:1、图片内存缓存;2、位图池内存缓存
    • 那么考虑一下怎么理解 计数 or 计量 ?针对计数战略运用Lru仅仅只计算缓存单元的个数,针对计量则要杂乱一点。
  • LruCache战略能否增加灵敏性

    • 在缓存容量满时筛选,除了这个战略之外,能否再增加一些辅佐战略,例如在 Java 堆内存达到某个阈值后,对 LruCache 运用愈加急进的整理战略。

    • 比方:Glide 除了选用 LRU 战略筛选最早的数据外,还会依据体系的内存紧张等级 onTrimMemory(level) 及时削减乃至清空 LruCache。

      /**

      • 这儿是参阅glide中的lru缓存战略,低内存的时分铲除

      • @param level level等级 */ public void trimMemory(int level) { if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) { clearMemory(); } else if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN || level == android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) { trimToSize(maxSize() / 2); } }

    • 关于Lru更多的原理解读,能够看:AppLruCache

3.4 DiskLru原理剖析

  • DiskLruCache 用于完结存储设备缓存,即磁盘缓存,它经过将缓存方针写入文件体系从而完结缓存的效果。
    • DiskLruCache最大的特点便是耐久化存储,一切的缓存以文件的办法存在。在用户进入APP时,它依据日志文件将DiskLruCache康复到用户上次退出时的状况,日志文件journal保存每个文件的下载、拜访和移除的信息,在康复缓存时逐行读取日志并检查文件来康复缓存。
  • DiskLruCache缓存根底原理流程图
    • 通用缓存存储设计方案
    • 关于DiskLruCache更多的原理解读,能够看:AppLruDisk

3.5 DataStore剖析

  • 为何会有DataStore
    • DataStore 被创造出来的方针便是代替 Sp,而它处理的 SharedPreferences 最大的问题有两点:一是功用问题,二是回调问题。
  • DataStore优势是异步Api
    • DataStore 的首要优势之一是异步API,所以本身并未供给同步API调用,但实践上或许不一定一向能将周围的代码更改为异步代码。
  • 提出一个问题和考虑
    • 假如运用现有代码库选用同步磁盘 I/O,或许您的依靠项不供给异步API,那么怎么将DataStore存储数据改成同步调用?
  • 运用堵塞式协程消除异步差异
    • 运用 runBlocking() 从 DataStore 同步读取数据。runBlocking()会运转一个新的协程并堵塞当时线程直到内部逻辑完结,所以尽量避免在UI线程调用。
  • 频频运用堵塞式协程会有问题吗
    • 要留意的一点是,不用在初始读取时调用runBlocking,会堵塞当时履行的线程,由于初始读取会有较多的IO操作,耗时较长。
    • 更为推荐的做法则是先异步读取到内存后,后续有需求可直接从内存中拿,而非运转同步代码堵塞式获取。

3.6 HashMap存储剖析

  • 内存缓存的场景
    • 比方 SharedPreferences 存储中,就做了内存缓存的操作。

3.7 Sqlite存储剖析

  • 留意:缓存的数据库是存放在/data/data/databases/目录下,是占用内存空间的,假如缓存累计,简略糟蹋内存,需求及时整理缓存。

3.8 运用缓存留意点

  • 在运用内存缓存的时分须要留意避免内存走漏,运用磁盘缓存的时分留意确保缓存的时效性
  • 针对SharedPreferences运用主张有:
    • 由于 SharedPreferences 尽管是全量更新的办法,但只要把保存的数据用适宜的逻辑拆分到多个不同的文件里,全量更新并不会对功用形成太大的连累。
    • 它规划初衷是轻量级,主张当存储文件中key-value数据超越30个,假如超越30个(这个只是一个假设),则开辟一个新的文件进行存储。主张不同事务模块的数据分文件存储……
  • 针对MMKV运用主张有:
    • 假如项目中有高频率,同步存储数据,运用MMKV愈加友好。
  • 针对DataStore运用主张有:
    • 主张在初始化的时分,运用全局上下文Context给DataStore设置存储路径。
  • 针对LruCache缓存运用主张:
    • 假如你运用“计量”筛选战略,需求重写 SystemLruCache#sizeOf() 测量缓存单元的内存占用量,否则缓存单元的巨细默许视为 1,相当于 maxSize 表示的是最大缓存数量。

3.9 各种数据存储文件

  • SharedPreferences 存储文件格局如下所示

    <?xml version='1.0' encoding='utf-8' standalone='yes' ?>
    <map>
        <string name="name">杨充</string>
        <int name="age" value="28" />
        <boolean name="married" value="true" />
    </map>
    
  • MMKV 存储文件格局如下所示

    • MMKV的存储结构,分了两个文件,一个数据文件,一个校验文件crc结尾。大概如下所示:
    • 通用缓存存储设计方案
    • 通用缓存存储设计方案
    • 这种规划最直接问题便是占用空间变大了许多,举一个比方,只存储了一个字段,可是为了便利MMAP映射,磁盘直接占用了8k的存储。
  • LruDiskCache 存储文件格局如下所示

    • 通用缓存存储设计方案
  • DataStore 存储文件格局如下所示

    • 通用缓存存储设计方案

04.通用缓存计划思路

4.1 怎么兼容不同缓存

  • 界说通用的存储接口
    • 不同的存储计划,由于api不一样,所以难以切换操作。要是想兼容不同存储计划切换,就必须自己制定一个通用缓存接口。
    • 界说接口,然后各个不同存储计划完结接口,重写笼统办法。调用的时分,获取接口方针调用api,这样就能够一致Api
  • 界说一个接口,这个接口有什么呢?
    • 首要是存和取各种根底类型数据,比方saveInt/readInt;saveString/readString等通用笼统办法

4.2 打造通用缓存Api

  • 通用缓存Api规划思路:

    • 通用一套api + 不同接口完结 + 署理类 + 工厂模型
  • 界说缓存的通用API接口,这儿省略部分代码

    interface ICacheable {
        fun saveXxx(key: String, value: Int)
        fun readXxx(key: String, default: Int = 0): Int
        fun removeKey(key: String)
        fun totalSize(): Long
        fun clearData()
    }
    
  • 根据接口而非完结编程的规划思维

    • 将接口和完结相分离,封装不安稳的完结,暴露安稳的接口。上游体系面向接口而非完结编程,不依靠不安稳的完结细节,这样当完结发生变化的时分,上游体系的代码基本上不需求做改动,以此来下降耦合性,进步扩展性。

4.3 切换不同缓存办法

  • 传入不同类型便利创立不同存储办法

    • 隐藏存储计划创立详细细节,开发者只需求关怀所需产品对应的工厂,无须关怀创立细节,乃至无须知道详细存储计划的类名。需求符合开闭准则
  • 那么详细该怎么完结呢?

    • 看到下面代码是不是有种很熟悉的感觉,没错,正是运用了工厂办法,灵敏切换不同的缓存办法。但针对运用层调用api却感知不到影响。

      public static ICacheable getCacheImpl(Context context, @CacheConstants.CacheType int type) { if (type == CacheConstants.CacheType.TYPE_DISK) { return DiskFactory.create().createCache(context); } else if (type == CacheConstants.CacheType.TYPE_LRU) { return LruCacheFactory.create().createCache(context); } else if (type == CacheConstants.CacheType.TYPE_MEMORY) { return MemoryFactory.create().createCache(context); } else if (type == CacheConstants.CacheType.TYPE_MMKV) { return MmkvFactory.create().createCache(context); } else if (type == CacheConstants.CacheType.TYPE_SP) { return SpFactory.create().createCache(context); } else if (type == CacheConstants.CacheType.TYPE_STORE) { return StoreFactory.create().createCache(context); } else { return MmkvFactory.create().createCache(context); } }

4.4 缓存的过期处理

  • 说一个运用场景
    • 比方你预备做WebView的资源拦截缓存,针对模版页面,为了提交加载速度。会缓存css,js,图片等资源到本地。那么怎么挑选存储计划,怎么处理过期问题?
  • 考虑一下该问题
    • 比方WebView缓存计划是数据库存储,db文件。针对缓存数据,猜测思路或许是Lru战略,或许标记时刻铲除过期文件。
  • 那么缓存过期处理的战略有哪些
    • 定时过期:每个设置过期时刻的key都需求创立⼀个定时器,到过期时刻就会当即铲除。
    • 惰性过期:只有当拜访⼀个 key 时,才会判别该key是否已过期,过期则铲除。
    • 定时过期:每隔⼀定的时刻,会扫描⼀定数量的数据库的 expires 字典中⼀定数量的key(是随机的), 并 铲除其中已过期的key 。
    • 分桶战略:定时过期的优化,将过期时刻点相近的 key 放在⼀起,按时刻扫描分桶。
    • 通用缓存存储设计方案

4.5 缓存的阀值处理

  • 筛选一个最早的节点就满足吗?以Lru缓存为事例做剖析……
    • 规范的 LRU 战略中,每次增加数据时最多只会筛选一个数据,但在 LRU 内存缓存中,只筛选一个数据单元往往并不行。
    • 例如在运用 “计量” 的内存图片缓存中,在参加一个大图片后,只筛选一个图片数据有或许依然达不到最大缓存容量约束。
  • 那么在LRUCache该怎么做呢?
    • 在复用 LinkedHashMap 完结 LRU 内存缓存时,前文提到的 LinkedHashMap#removeEldestEntry() 筛选判别接口或许就不行看了,由于它每次最多只能筛选一个数据单元。
  • LruCache是怎么处理这个问题
    • 这个当地就需求重写LruCache中的sizeOf()办法,然后拿到key和value方针计算其内存巨细。

4.6 缓存的线程安全性

  • 为何要着重缓存计划线程安全性
    • 缓存虽好,用起来很便利便利,但在运用过程中,大家一定要留意数据更新和线程安全,不要出现脏数据。
  • 针对LruCache中运用LinkedHashMap读写不安全状况
    • 确保LruCache的线程安全,在put,get等中心办法中,增加synchronized锁。这儿首要是synchronized (this){ put操作 }
  • 针对DiskLruCache读写不安全的状况
    • DiskLruCache 管理多个 Entry(key-values),因而锁粒度应该是 Entry 等级的。
    • get 和 edit 办法都是同步办法,确保内部的 Entry Map 的安全拜访,是确保线程安全的第一步。

4.7 缓存数据的搬迁

  • 怎么将Sp数据搬迁到DataStore

    • 经过特点委托的办法创立DataStore,根据已有的SharedPreferences文件进行创立DataStore。将sp文件名,以参数的办法传入preferencesDataStore,DataStore会主动将该文件中的数据进行转化。

      val Context.dataStore: DataStore by preferencesDataStore( name = “user_info”, produceMigrations = { context -> listOf(SharedPreferencesMigration(context, “sp_file_name”)) })

  • 怎么将sp数据搬迁到MMKV

    • MMKV 供给了 importFromSharedPreferences() 函数,能够比较便利地搬迁数据过来。

    • MMKV 还额外完结了一遍 SharedPreferences、SharedPreferences.Editor 这两个 interface。

      MMKV preferences = MMKV.mmkvWithID(“myData”); // 搬迁旧数据 { SharedPreferences old_man = getSharedPreferences(“myData”, MODE_PRIVATE); preferences.importFromSharedPreferences(old_man); old_man.edit().clear().commit(); }

  • 考虑一下,MMKV结构完结了sp的两个接口,即磨平了数据搬迁差异性

    • 那么运用这个办法,借鉴该思路,你能否尝试用该办法,去完结LruDiskCache计划的sp数据一键搬迁。

4.8 缓存数据加密

  • 考虑一下,假如让你去规划数据的加密,你该怎么做?
    • 详细能够参阅MMKV的数据加密过程。

4.9 缓存功率的比照

  • 测验数据
    • 测验写入和读取。留意别离运用不同的办法,测验存储或获取相同的数据(数据为int类型数字,还有String类型长字符串)。然后检查耗时时刻的长短……
  • 比较方针
    • SharePreferences/DataStore/MMKV/LruDisk/Room。运用华为手机测验
  • 测验数据事例1
    • 通用缓存存储设计方案
    • 通用缓存存储设计方案
    • 在主线程中测验数据,同步耗时时刻(主线程还有其他的耗时)跟异步场景有较大不同。
  • 测验数据事例2
    • 通用缓存存储设计方案
    • 测验1000组长字符串数据,MMKV 就不具备优势了,反而成了耗时最久的;而这时分的冠军就成了 DataStore,并且是遥遥领先。
  • 终究考虑阐明
    • 从终究的数据来看,这几种计划都不是很慢。尽管这半秒左右的主线程耗时看起来很可怕,可是要知道这是 1000 次连续写入的耗时。
    • 而在真实写程序的时分,怎么会一次性做 1000 次的长字符串的写入?所以真实在项目中的键值对写入的耗时,不论你选哪个计划,都会比这份测验结果的耗时少得多的,都少到了能够疏忽的程度,这是要害。

05.计划根底规划

5.1 全体架构图

  • 一致存储计划的架构图
    • 通用缓存存储设计方案

5.2 UML规划图

  • 通用存储计划UML规划图
    • 通用缓存存储设计方案

5.3 代码阐明图

  • 项目中代码相关阐明图
    • 通用缓存存储设计方案

5.4 要害流程图

  • mmap的零拷贝流程图
    • 通用缓存存储设计方案

5.5 模块间依靠联系

  • 存储库依靠的联系
    • MMKV需求依靠一些腾讯开源库的服务;DataStore存储需求依靠datastore相关的库;LruDisk存储需求依靠disk库
    • 假如你要拓宽其他的存储计划,则需求增加其依靠。需求留意,增加的库运用compileOnly。

06.其他规划阐明

6.1 功用规划

  • 关于根底库功用怎么考量
    • 详细功用能够参阅测验功率的比照。

6.2 安稳性规划

  • 针对多进程初始化
    • 遇到问题:关于多进程在Application的onCreate创立几回,导致缓存存储库初始化了屡次。
    • 问题剖析:该场景不是该库的问题,主张判别是否是主进程,假如是则进行初始化。
    • 怎么处理:思路是获取当时进程名,并与主进程比照,来判别是否为主进程。详细能够参阅:高雅判别是否是主进程

6.3 灰度规划

  • 暂无灰度规划

6.4 降级规划

  • 由于缓存办法众多,在该库中装备了降级,怎么设置降级

    //设置是否是debug办法
    CacheConfig cacheConfig = builder.monitorToggle(new IMonitorToggle() {
            @Override
            public boolean isOpen() {
                //todo 是否降级,假如降级,则不运用该功用。留给AB测验开关
                return true;
            }
        })
        //创立
        .build();
    CacheInitHelper.INSTANCE.init(this,cacheConfig);
    
  • 降级后的逻辑处理是

    • 假如是降级逻辑,则默许运用谷歌官方存储结构SharedPreferences。默许是不会降级的!

      if (CacheInitHelper.INSTANCE.isToggleOpen()){ //假如是降级,则默许运用sp return SpFactory.create().createCache(); }

6.5 反常规划阐明

  • DataStore初始化遇到的坑
    • 遇到问题:不能将DataStore初始化代码写到Activity里边去,否则重复进入Activity并运用Preferences DataStore时,会尝试去创立一个同名的.preferences_pb文件。
    • 问题剖析:SingleProcessDataStore#check(!activeFiles.contains(it)),该办法会检查假如判别到activeFiles里已经有该文件,直接抛反常,即不允许重复创立。
    • 怎么处理:在项目中只在顶层调用一次 preferencesDataStore 办法,这样能够更轻松地将 DataStore 保存为单例。
  • MMKV遇到的坑阐明
    • MMKV 是有数据损坏的概率的,MMKV 的 GitHub wiki 页面显示,微信的 iOS 版均匀每天有 70 万次的数据校验不经过(即数据损坏)。

6.6 兼容性规划

  • MMKV数据搬迁比较难
    • MMKV都是按字节进行存储的,实践写入文件把类型擦除了,这也是MMKV不支撑getAll的原因,尽管说getAll用的不多问题不大,可是MMKV因而就不具备导出和搬迁的能力。
    • 比较好的计划是每次存储,多用一个字节来存储数据类型,这样占用的空间也不会大许多,可是具备了更好的可扩展性。

6.7 自测性规划

  • MMKV不太便利检查数据和解析数据
    • 官方现在支撑了5个渠道,Android、iOS、Win、MacOS、python,可是没有供给解析数据的东西,数据文件和crc都是字节码,除了中文能看出一些内容,直接检查仍是存在许多乱码。
    • 比方线上出了问题,把用户的存储文件捞上来,还得替换到体系目录里,经过代码断点去看,这也太不便利了。
  • Sp,FastSp,DiskCache,Store等支撑检查文件解析数据
    • 傻瓜式的检查缓存文件,操作缓存文件。详细看该库:MonitorFileLib磁盘检查东西

07.通用Api规划

7.1 怎么依靠该库

  • 依靠该库如下所示

    //通用缓存存储库,支撑sp,fastsp,mmkv,lruCache,DiskLruCache等
    implementation 'com.github.yangchong211.YCCommonLib:AppBaseStore:1.4.8'
    

7.2 初始化缓存库

  • 通用存储库初始化

    CacheConfig.Builder builder = CacheConfig.Companion.newBuilder();
    //设置是否是debug办法
    CacheConfig cacheConfig = builder.debuggable(BuildConfig.DEBUG)
            //设置外部存储根目录
            .extraLogDir(null)
            //设置lru缓存最大值
            .maxCacheSize(100)
            //内部存储根目录
            .logDir(null)
            //创立
            .build();
    CacheInitHelper.INSTANCE.init(MainApplication.getInstance(),cacheConfig);
    //最简略的初始化
    //CacheInitHelper.INSTANCE.init(CacheConfig.Companion.newBuilder().build());
    

7.3 切换各种缓存计划

  • 怎么调用api切换各种缓存计划

    //这儿能够填写不同的type
    val cacheImpl = CacheFactoryUtils.getCacheImpl(CacheConstants.CacheType.TYPE_SP)
    

7.4 数据的存和取

  • 存储数据和获取数据

    //存储数据
    dataCache.saveBoolean("cacheKey1",true);
    dataCache.saveFloat("cacheKey2",2.0f);
    dataCache.saveInt("cacheKey3",3);
    dataCache.saveLong("cacheKey4",4);
    dataCache.saveString("cacheKey5","doubi5");
    dataCache.saveDouble("cacheKey6",5.20);
    //获取数据
    boolean data1 = dataCache.readBoolean("cacheKey1", false);
    float data2 = dataCache.readFloat("cacheKey2", 0);
    int data3 = dataCache.readInt("cacheKey3", 0);
    long data4 = dataCache.readLong("cacheKey4", 0);
    String data5 = dataCache.readString("cacheKey5", "");
    double data6 = dataCache.readDouble("cacheKey5", 0.0);
    
  • 也能够经过注解的办法存储数据

    class NormalCache : DataCache() {
        @BoolCache(KeyConstant.HAS_ACCEPTED_PARENT_AGREEMENT, false)
        var hasAcceptParentAgree: Boolean by this
    }
    //怎么运用
    object CacheHelper {
        //常规缓存数据,记载一些重要的信息,慎重铲除数据
        private val normal: NormalCache by lazy {
            NormalCache().apply {
                setCacheImpl(
                    DiskCache.Builder()
                        .setFileId("NormalCache")
                        .build()
                )
            }
        }
        fun normal() = normal
    }
    //存数据
    CacheHelper.normal().hasAcceptParentAgree = true
    //取数据
    val hasAccept = CacheHelper.normal().hasAcceptParentAgree
    

7.5 检查缓存文件数据

  • android缓存路径检查办法有哪些呢?
    • 将手机翻开开发者办法并衔接电脑,在pc控制台输入cd /data/data/目录,运用adb首要是便利测验(删除,检查,导出都比较麻烦)。
    • 怎么简略快速,傻瓜式的检查缓存文件,操作缓存文件,那么该项目小东西就非常有必要呢!选用可视化界面读取缓存数据,便利操作,直观也简略。
  • 一键接入该东西
    • FileExplorerActivity.startActivity(this);
    • 开源项目地址:github.com/yangchong21…
  • 检查缓存文件数据如下所示
    • 通用缓存存储设计方案

7.6 怎么挑选适宜计划

  • 比方常见的缓存、浏览器缓存、图片缓存、线程池缓存、或许WebView资源缓存等等
    • 那就能够挑选LRU+缓存筛选算法。它的中心思维是当缓存满时,会优先筛选那些近期最少运用的缓存方针。
  • 比方针对高频率,同步存储,或许跨进程等存储数据的场景
    • 那就能够挑选MMKV这种存储计划。它的中心思维便是高速存储数据,且不会堵塞主线程卡顿。
  • 比方针对存储表结构,或许一对多这类的数据
    • 那就能够挑选DataStore,Room,GreenDao等存储库计划。
  • 比方针对存储少数用户类数据
    • 其实也能够将json转化为字符串,然后挑选sp,mmkv,lruDisk等等都能够。

08.其他阐明介绍

8.1 遇到的坑剖析

  • Sp存储数据commit() / apply()操作或许会形成ANR问题
    • commit()是同步提交,会在UI主线程中直接履行IO操作,当写入操作耗时比较长时就会导致UI线程被堵塞,从而发生ANR;

    • apply()尽管是异步提交,但异步写入磁盘时,假如履行了Activity / Service中的onStop()办法,那么一样会同步等候SP写入完毕,等候时刻过长时也会引起ANR问题。

    • 首先剖析一下SharedPreferences源码中apply办法

      SharedPreferencesImpl#apply(),这个办法首要是将记载的数据同步写到Map调集中,然后在开启子线程将数据写入磁盘
      SharedPreferencesImpl#enqueueDiskWrite(),这个会将runnable被写入了行列,然后在run办法中写数据到磁盘
      QueuedWork#queue(),这个将runnable增加到sWork(LinkedList链表)中,然后经过handler发送处理行列音讯MSG_RUN

    • 然后再看一下ActivityThread源码中的handlePauseActivity()、handleStopActivity()办法。

      ActivityThread#handlePauseActivity()/handleStopActivity(),Activity在pause和stop的时分会调用该办法
      ActivityThread#handlePauseActivity()#QueuedWork.waitToFinish(),这个是等候QueuedWork一切任务处理完的逻辑
      QueuedWork#waitToFinish(),这个里边会经过handler查询MSG_RUN音讯是否有,假如有则会waiting等候

    • 那么终究得出的结论是

      • handlePauseActivity()的时分会一向等候 apply() 办法将数据保存成功,否则会一向等候,从而堵塞主线程形成 ANR。但普通存储的场景,这种或许性很小。

8.2 项目开发共享

  • 通用缓存存储库开源代码
    • github.com/yangchong21…