96SEO 2026-02-19 20:33 17
有的用sp有的用mmkv有的用lru有的用DataStore有的用sqlite如何打造通用api切换操作不同存储方案

针对不同场景选择什么缓存方式同时思考如何替换之前老的存储方案而不用花费很大的时间成本
屏蔽各种缓存方式的差异性暴露给外部开发者统一的API外部开发者简化使用提高开发效率和使用效率……
问题1各种缓存方案分别是如何保证数据安全的其内部使用到了哪些锁由于引入锁给效率上带来了什么影响问题2各种缓存方案进程不安全是否会导致数据丢失如何处理数据丢失情况如何处理脏数据其原理大概是什么问题3各种缓存方案使用场景是什么有什么缺陷为了解决缺陷做了些什么比如sp存在缺陷的替代方案是DataStore为何这样问题4各种缓存方案他们的缓存效率是怎样的如何对比接入该库后如何做数据迁移如何覆盖操作
问题1-线程安全使用K-V存储一般会在多线程环境中执行因此框架有必要保证多线程并发安全并且优化并发效率问题2-内存缓存由于磁盘
操作是耗时操作因此框架有必要在业务层和磁盘文件之间增加一层内存缓存问题3-事务由于磁盘
操作聚合为一次磁盘写回事务减少访问磁盘次数问题4-事务串行化由于程序可能由多个线程发起写回事务因此框架有必要保证事务之间的事务串行化避免先执行的事务覆盖后执行的事务问题5-异步或同步写回由于磁盘
是耗时操作因此框架有必要支持后台线程异步写回有时候又要求数据读写是同步的问题6-增量更新由于磁盘文件内容可能很大因此修改
时有必要支持局部修改而不是全量覆盖修改问题7-变更回调由于业务层可能有监听
变更的需求因此框架有必要支持变更回调监听并且防止出现内存泄漏问题8-多进程由于程序可能有多进程需求那么框架如何保证多进程数据同步问题9-可用性由于程序运行中存在不可控的异常和
Crash因此框架有必要尽可能保证系统可用性尽量保证系统在遇到异常后的数据完整性问题10-高效性性能永远是要考虑的问题解析、读取、写入和序列化的性能如何提高和权衡问题11-安全性如果程序需要存储敏感数据如何保证数据完整性和保密性问题12-数据迁移如果项目中存在旧框架如何将数据从旧框架迁移至新框架并且保证可靠性问题13-研发体验是否模板代码冗长是否容易出错。
各种K—V框架使用体验如何
提及缓存可能很容易想到Http的缓存机制LruCache其实缓存最初是针对于网络而言的也是狭义上的缓存广义的缓存是指对数据的复用。
每一种缓存总会有一个最大的容量到达这个限度以后那么就须要进行缓存清理了框架。
这个时候就需要删除一些旧的缓存并添加新的缓存。
设计一个缓存通用方案其次它的结构需要很简单因为很多地方需要用到再次它得线程安全。
灵活切换不同的缓存方式使用简单。
作为技术沉淀当作专项来推动进展。
高复用低耦合便于拓展可快速移植解决各个项目使用内存缓存spmmkvsqllruDataStore的凌乱。
抽象一套统一的API接口。
打造通用api抹平了spmmkvsqllrudataStore等各种方案的差异性。
简化开发者使用功能强大而使用简单
内存缓存这里的内存主要指的存储器缓存磁盘缓存这里主要指的是外部存储器手机的话指的就是存储卡。
通过预先消耗应用的一点内存来存储数据便可快速的为应用中的组件提供数据是一种典型的以空间换时间的策略。
读取磁盘文件要比直接从内存缓存中读取要慢一些而且需要在一个UI主线程外的线程中进行因为磁盘的读取速度是不能够保证的磁盘文件缓存显然也是一种以空间换时间的策略。
内存缓存和磁盘缓存结合。
比如LruCache将图片保存在内存存取速度较快退出APP后缓存会失效而DiskLruCache将图片保存在磁盘中下次进入应用后缓存依旧存在它的存取速度相比LruCache会慢上一些。
一般来说缓存核心步骤主要包含缓存的添加、获取和删除这三类操作。
那么为什么还要删除缓存呢不管是内存缓存还是硬盘缓存它们的缓存大小都是有限的。
当缓存满了之后再想其添加缓存这个时候就需要删除一些旧的缓存并添加新的缓存。
这个跟线程池满了以后的线程处理策略相似
used)最少使用策略RecyclerView的缓存采用了此策略。
LRU(least
used):最近最少使用策略Glide在进行内存缓存的时候采用了此策略。
内存缓存存储在内存中如果对象销毁则内存也会跟随销毁。
如果是静态对象那么进程杀死后内存会销毁。
磁盘缓存后台应用有可能会被杀死那么相应的内存缓存对象也会被销毁。
当你的应用重新回到前台显示时你需要用到缓存数据时这个时候可以用磁盘缓存。
SharedPreferencesMMKVDiskLruCacheSqlLiteDataStoreRoomRealmGreenDao等等
Map内存缓存一般用HashMap存储一些数据主要存储一些临时的对象LruCache内存淘汰缓存内部使用LinkedHashMap会淘汰最长时间未使用的对象
SharedPreferences轻量级磁盘存储一般存储配置属性线程安全。
建议不要存储大数据不支持跨进程MMKV腾讯开源存储库内部采用mmap。
DiskLruCache磁盘淘汰缓存写入数据到file文件SqlLite移动端轻量级数据库。
主要是用来对象持久化存储。
DataStore旨在替代原有的
SharedPreferences支持SharedPreferences数据的迁移Room/Realm/GreenDao支持大型或复杂数据集
1.SP用内存层用HashMap保存磁盘层则是用的XML文件保存。
每次更改都需要将整个HashMap序列化为XML格式的报文然后整个写入文件。
2.SP读写文件不是类型安全的且没有发出错误信号的机制缺少事务性API3.commit()
1.没有类型信息不支持getAll。
由于没有记录类型信息MMKV无法自动反序列化也就无法实现getAll接口。
2.需要引入so增加包体积引入MMKV需要增加的体积还是不少的。
3.文件只增不减MMKV的扩容策略还是比较激进的而且扩容之后不会主动trim
1.只是提供异步API没有提供同步API方法。
在进行大量同步存储的时候使用runBlocking同步数据可能会卡顿。
2.对主线程执行同步
SharedPreferences它是一个轻量级的存储类特别适合用于保存软件配置参数。
轻量级以键值对的方式进行存储。
采用的是xml文件形式存储在本地程序卸载后会也会一并被清除不会残留信息。
线程安全的。
对文件IO读取因此在IO上的瓶颈是个大问题因为在每次进行get和commit时都要将数据从内存写入到文件中或从文件中读取。
多线程场景下效率较低在get操作时会锁定SharedPreferences对象互斥其他操作而当putcommit时则会锁定Editor对象使用写入锁进行互斥在这种情况下效率会降低。
不支持跨进程通讯由于每次都会把整个文件加载到内存中不建议存储大的文件内容比如大json。
建议不要存储较大数据频繁修改的数据修改后统一提交而不是修改过后马上提交在跨进程通讯中不去使用键值对不宜过多
第一次通过Context.getSharedPreferences()进行初始化时对xml文件进行一次读取并将文件内所有内容即所有的键值对缓到内存的一个Map中接下来所有的读操作只需要从这个Map中取就可以
微信聊天对话内容中的特殊字符所导致的程序崩溃是一类很常见、也很需要快速解决的问题而哪些字符会导致程序崩溃是无法预知的。
只能等用户手机上的微信崩溃之后再利用类似时光倒流的回溯行为看看上次软件崩溃的最后一瞬间用户收到或者发出了什么消息再用这些消息中的文字去尝试复现发生过的崩溃最终试出有问题的字符然后针对性解决。
考量1把聊天页面的显示文字写到手机磁盘里才能在程序崩溃、重新启动之后通过读取文件的方式来查看。
但这种方式涉及到io流读写且消息多会有性能问题。
考量2App程序都崩溃了如何保证要存储的内容都写入到磁盘中呢考量3保存聊天内容到磁盘的行为这个做成同步还是异步呢如果是异步如何保证聊天消息的时序性考量4如何存储数据是同步行为针对群里聊天这么多消息如何才能避免卡顿呢考量5存储数据放到主线程中用户在群聊天页面猛滑消息如何爆发性集中式对磁盘写入数据
在性能和空间占用上都有不错的表现。
写入优化考虑到主要使用场景是频繁地进行写入更新需要有增量更新的能力。
考虑将增量
针对该业务高频率同步大量数据写入磁盘的需求。
不管用sp还是store还是disk还是数据库只要在主线程同步写入磁盘会很卡。
解决方案就是使用内存映射mmap的底层方法相当于系统为指定文件开辟专用内存空间内存数据的改动会自动同步到文件里。
用浅显的话说MMKV就是实现用「写入内存」的方式来实现「写入磁盘」的目标。
内存的速度多快呀耗时几乎可以忽略这样就把写磁盘造成卡顿的问题解决了。
在LruCache的源码中关于LruCache有这样的一段介绍
cache对象通过一个强引用来访问内容。
每次当一个item被访问到的时候这个item就会被移动到一个队列的队首。
当一个item被添加到已经满了的队列时这个队列的队尾的item就会被移除。
LRU是近期最少使用的算法它的核心思想是当缓存满时会优先淘汰那些近期最少使用的缓存对象。
采用LRU算法的缓存有两种LrhCache和DiskLruCache分别用于实现内存缓存和硬盘缓存其核心思想都是LRU缓存算法。
个空闲连接3、数据库连接池使用计量策略1、图片内存缓存2、位图池内存缓存那么思考一下如何理解
针对计数策略使用Lru仅仅只统计缓存单元的个数针对计量则要复杂一点。
在缓存容量满时淘汰除了这个策略之外能否再增加一些辅助策略例如在
android.content.ComponentCallbacks2.TRIM_MEMORY_BACKGROUND)
android.content.ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN
android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL)
用于实现存储设备缓存即磁盘缓存它通过将缓存对象写入文件系统从而实现缓存的效果。
DiskLruCache最大的特点就是持久化存储所有的缓存以文件的形式存在。
在用户进入APP时它根据日志文件将DiskLruCache恢复到用户上次退出时的情况日志文件journal保存每个文件的下载、访问和移除的信息在恢复缓存时逐行读取日志并检查文件来恢复缓存。
关于DiskLruCache更多的原理解读可以看AppLruDisk
的主要优势之一是异步API所以本身并未提供同步API调用但实际上可能不一定始终能将周围的代码更改为异步代码。
I/O或者您的依赖项不提供异步API那么如何将DataStore存储数据改成同步调用
同步读取数据。
runBlocking()会运行一个新的协程并阻塞当前线程直到内部逻辑完成所以尽量避免在UI线程调用。
要注意的一点是不用在初始读取时调用runBlocking会阻塞当前执行的线程因为初始读取会有较多的IO操作耗时较长。
更为推荐的做法则是先异步读取到内存后后续有需要可直接从内存中拿而非运行同步代码阻塞式获取。
注意缓存的数据库是存放在/data/data/databases/目录下是占用内存空间的如果缓存累计容易浪费内存需要及时清理缓存。
在使用内存缓存的时候须要注意防止内存泄露使用磁盘缓存的时候注意确保缓存的时效性针对SharedPreferences使用建议有
虽然是全量更新的模式但只要把保存的数据用合适的逻辑拆分到多个不同的文件里全量更新并不会对性能造成太大的拖累。
它设计初衷是轻量级建议当存储文件中key-value数据超过30个如果超过30个这个只是一个假设则开辟一个新的文件进行存储。
建议不同业务模块的数据分文件存储……
建议在初始化的时候使用全局上下文Context给DataStore设置存储路径。
MMKV的存储结构分了两个文件一个数据文件一个校验文件crc结尾。
大概如下所示这种设计最直接问题就是占用空间变大了很多举一个例子只存储了一个字段但是为了方便MMAP映射磁盘直接占用了8k的存储。
不同的存储方案由于api不一样所以难以切换操作。
要是想兼容不同存储方案切换就必须自己制定一个通用缓存接口。
定义接口然后各个不同存储方案实现接口重写抽象方法。
调用的时候获取接口对象调用api这样就可以统一Api
主要是存和取各种基础类型数据比如saveInt/readIntsaveString/readString等通用抽象方法
将接口和实现相分离封装不稳定的实现暴露稳定的接口。
上游系统面向接口而非实现编程不依赖不稳定的实现细节这样当实现发生变化的时候上游系统的代码基本上不需要做改动以此来降低耦合性提高扩展性。
隐藏存储方案创建具体细节开发者只需要关心所需产品对应的工厂无须关心创建细节甚至无须知道具体存储方案的类名。
需要符合开闭原则
看到下面代码是不是有种很熟悉的感觉没错正是使用了工厂模式灵活切换不同的缓存方式。
但针对应用层调用api却感知不到影响。
CacheConstants.CacheType.TYPE_DISK)
DiskFactory.create().createCache(context);}
CacheConstants.CacheType.TYPE_LRU)
LruCacheFactory.create().createCache(context);}
CacheConstants.CacheType.TYPE_MEMORY)
MemoryFactory.create().createCache(context);}
CacheConstants.CacheType.TYPE_MMKV)
MmkvFactory.create().createCache(context);}
CacheConstants.CacheType.TYPE_SP)
SpFactory.create().createCache(context);}
CacheConstants.CacheType.TYPE_STORE)
StoreFactory.create().createCache(context);}
MmkvFactory.create().createCache(context);}
比如你准备做WebView的资源拦截缓存针对模版页面为了提交加载速度。
会缓存cssjs图片等资源到本地。
那么如何选择存储方案如何处理过期问题
比如WebView缓存方案是数据库存储db文件。
针对缓存数据猜想思路可能是Lru策略或者标记时间清除过期文件。
定时过期每个设置过期时间的key都需要创建⼀个定时器到过期时间就会立即清除。
惰性过期只有当访问⼀个
时才会判断该key是否已过期过期则清除。
定期过期每隔⼀定的时间会扫描⼀定数量的数据库的
的内存图片缓存中在加入一个大图片后只淘汰一个图片数据有可能依然达不到最大缓存容量限制。
LinkedHashMap#removeEldestEntry()
淘汰判断接口可能就不够看了因为它每次最多只能淘汰一个数据单元。
这个地方就需要重写LruCache中的sizeOf()方法然后拿到key和value对象计算其内存大小。
缓存虽好用起来很快捷方便但在使用过程中大家一定要注意数据更新和线程安全不要出现脏数据。
针对LruCache中使用LinkedHashMap读写不安全情况
保证LruCache的线程安全在putget等核心方法中添加synchronized锁。
这里主要是synchronized
通过属性委托的方式创建DataStore基于已有的SharedPreferences文件进行创建DataStore。
将sp文件名以参数的形式传入preferencesDataStoreDataStore会自动将该文件中的数据进行转换。
-listOf(SharedPreferencesMigration(context,
SharedPreferences、SharedPreferences.Editor
MODE_PRIVATE);preferences.importFromSharedPreferences(old_man);old_man.edit().clear().commit();
}思考一下MMKV框架实现了sp的两个接口即磨平了数据迁移差异性
那么使用这个方式借鉴该思路你能否尝试用该方法去实现LruDiskCache方案的sp数据一键迁移。
测试写入和读取。
注意分别使用不同的方式测试存储或获取相同的数据(数据为int类型数字还有String类型长字符串)。
然后查看耗时时间的长短……
SharePreferences/DataStore/MMKV/LruDisk/Room。
使用华为手机测试
在主线程中测试数据同步耗时时间(主线程还有其他的耗时)跟异步场景有较大差别。
从最终的数据来看这几种方案都不是很慢。
虽然这半秒左右的主线程耗时看起来很可怕但是要知道这是
次的长字符串的写入所以真正在项目中的键值对写入的耗时不管你选哪个方案都会比这份测试结果的耗时少得多的都少到了可以忽略的程度这是关键。
MMKV需要依赖一些腾讯开源库的服务DataStore存储需要依赖datastore相关的库LruDisk存储需要依赖disk库如果你要拓展其他的存储方案则需要添加其依赖。
需要注意添加的库使用compileOnly。
遇到问题对于多进程在Application的onCreate创建几次导致缓存存储库初始化了多次。
问题分析该场景不是该库的问题建议判断是否是主进程如果是则进行初始化。
如何解决思路是获取当前进程名并与主进程对比来判断是否为主进程。
具体可以参考优雅判断是否是主进程
由于缓存方式众多在该库中配置了降级如何设置降级//设置是否是debug模式
CacheInitHelper.INSTANCE.init(this,cacheConfig);降级后的逻辑处理是
如果是降级逻辑则默认使用谷歌官方存储框架SharedPreferences。
默认是不会降级的
(CacheInitHelper.INSTANCE.isToggleOpen()){//如果是降级则默认使用spreturn
SpFactory.create().createCache();
遇到问题不能将DataStore初始化代码写到Activity里面去否则重复进入Activity并使用Preferences
DataStore时会尝试去创建一个同名的.preferences_pb文件。
问题分析SingleProcessDataStore#check(!activeFiles.contains(it))该方法会检查如果判断到activeFiles里已经有该文件直接抛异常即不允许重复创建。
如何解决在项目中只在顶层调用一次
MMKV都是按字节进行存储的实际写入文件把类型擦除了这也是MMKV不支持getAll的原因虽然说getAll用的不多问题不大但是MMKV因此就不具备导出和迁移的能力。
比较好的方案是每次存储多用一个字节来存储数据类型这样占用的空间也不会大很多但是具备了更好的可扩展性。
官方目前支持了5个平台Android、iOS、Win、MacOS、python但是没有提供解析数据的工具数据文件和crc都是字节码除了中文能看出一些内容直接查看还是存在大量乱码。
比如线上出了问题把用户的存储文件捞上来还得替换到系统目录里通过代码断点去看这也太不方便了。
SpFastSpDiskCacheStore等支持查看文件解析数据
傻瓜式的查看缓存文件操作缓存文件。
具体看该库MonitorFileLib磁盘查看工具
依赖该库如下所示//通用缓存存储库支持spfastspmmkvlruCacheDiskLruCache等
com.github.yangchong211.YCCommonLib:AppBaseStore:1.4.87.2
CacheConfig.Companion.newBuilder();
builder.debuggable(BuildConfig.DEBUG)//设置外部存储根.logDir(null)//创建.build();
CacheInitHelper.INSTANCE.init(MainApplication.getInstance(),cacheConfig);
//CacheInitHelper.INSTANCE.init(CacheConfig.Companion.newBuilder().build());7.3
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);//获取数据
dataCache.readBoolean(cacheKey1,
dataCache.readString(cacheKey5,
dataCache.readDouble(cacheKey5,
{BoolCache(KeyConstant.HAS_ACCEPTED_PARENT_AGREEMENT,
{//常规缓存数据,记录一些重要的信息,慎重清除数据private
{setCacheImpl(DiskCache.Builder().setFileId(NormalCache).build())}}fun
CacheHelper.normal().hasAcceptParentAgree
CacheHelper.normal().hasAcceptParentAgree7.5
/data/data/目录使用adb主要是方便测试(删除查看导出都比较麻烦)。
如何简单快速傻瓜式的查看缓存文件操作缓存文件那么该项目小工具就非常有必要呢采用可视化界面读取缓存数据方便操作直观也简单。
FileExplorerActivity.startActivity(this);开源项目地址https://github.com/yangchong211/YCAndroidTool
比如常见的缓存、浏览器缓存、图片缓存、线程池缓存、或者WebView资源缓存等等
那就可以选择LRU缓存淘汰算法。
它的核心思想是当缓存满时会优先淘汰那些近期最少使用的缓存对象。
那就可以选择MMKV这种存储方案。
它的核心思想就是高速存储数据且不会阻塞主线程卡顿。
那就可以选择DataStoreRoomGreenDao等存储库方案。
其实也可以将json转化为字符串然后选择spmmkvlruDisk等等都可以。
commit()是同步提交会在UI主线程中直接执行IO操作当写入操作耗时比较长时就会导致UI线程被阻塞进而产生ANRapply()虽然是异步提交但异步写入磁盘时如果执行了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等待
https://github.com/yangchong211/YCCommonLib/tree/master/AppBaseStore
作为专业的SEO优化服务提供商,我们致力于通过科学、系统的搜索引擎优化策略,帮助企业在百度、Google等搜索引擎中获得更高的排名和流量。我们的服务涵盖网站结构优化、内容优化、技术SEO和链接建设等多个维度。
| 服务项目 | 基础套餐 | 标准套餐 | 高级定制 |
|---|---|---|---|
| 关键词优化数量 | 10-20个核心词 | 30-50个核心词+长尾词 | 80-150个全方位覆盖 |
| 内容优化 | 基础页面优化 | 全站内容优化+每月5篇原创 | 个性化内容策略+每月15篇原创 |
| 技术SEO | 基本技术检查 | 全面技术优化+移动适配 | 深度技术重构+性能优化 |
| 外链建设 | 每月5-10条 | 每月20-30条高质量外链 | 每月50+条多渠道外链 |
| 数据报告 | 月度基础报告 | 双周详细报告+分析 | 每周深度报告+策略调整 |
| 效果保障 | 3-6个月见效 | 2-4个月见效 | 1-3个月快速见效 |
我们的SEO优化服务遵循科学严谨的流程,确保每一步都基于数据分析和行业最佳实践:
全面检测网站技术问题、内容质量、竞争对手情况,制定个性化优化方案。
基于用户搜索意图和商业目标,制定全面的关键词矩阵和布局策略。
解决网站技术问题,优化网站结构,提升页面速度和移动端体验。
创作高质量原创内容,优化现有页面,建立内容更新机制。
获取高质量外部链接,建立品牌在线影响力,提升网站权威度。
持续监控排名、流量和转化数据,根据效果调整优化策略。
基于我们服务的客户数据统计,平均优化效果如下:
我们坚信,真正的SEO优化不仅仅是追求排名,而是通过提供优质内容、优化用户体验、建立网站权威,最终实现可持续的业务增长。我们的目标是与客户建立长期合作关系,共同成长。
Demand feedback