MMKV和SP、DataStore对比

1、细数SharedPreference的几宗罪

2018年末,输入法开始全面拥抱MMKV,SharedPreference在同一时间被标注为@Deprecated。4年过去了,SharedPreference仍然活跃在我们面前。



SharedPreference(下文简称SP)是Android平台第一个键值对存储方案,使用非常方便。底层使用xml的结构进行数据存储,所有的数据都是明文的。

也许Google在设计之初对SP的定位就是一个轻量级的数据存储,所以没有考虑到数据量增大时可能带来的一系列问题。

细数SP在使用过程中的问题,包括以下几项:

  • 卡顿问题,通常发生在初始化及写入文件的场景。即使使用了异步写入(apply)的API,上述问题仍可能发生。在 8.0 之前和 8.0 之后实现各不相同
  • SharedPreference 不能保证类型安全
  • SharedPreference 加载的数据会一直留在内存中,浪费内存
  • SharedPreference的多进程是不安全的
  • 无法监听写入的结果
  • ......

下面着重介绍一下SP的卡顿问题,也是大家诟病最多的一个问题。其他几个缺点都比较好理解。



1.1 SP的卡顿问题

SP导致卡顿通常在以下两个场景中发生:

  • SP初始化时,会把文件加载到内存中。在未完成加载之前,尝试调用get来获取value
  • Google为确保SP跨进程通讯的安全性,会在组件的相关生命周期中,把SP中的缓存写入文件。



1.1.1 初始化导致的卡顿

在SharedPreferencesImpl类的构造函数中,会调用startLoadFromDisk方法,尝试把数据加载到内存中。方法如下:

而在SP的任意一个get方法中,都可以看到,在获取数据之前调用了:

await方法中会等待数据加载完成。如果SP的数据量太大,卡顿就不可避免了。

1.1.2 apply同样可能导致卡顿

每次调用SharedPreference的apply方法,都会创建一个runnable加入到QueueWork中。

以ActivityThread中handlePauseActivity为例,其中就有等待提交的commit完成的代码:

这样的代码在Service的创建、stop,和Activity的pause、stop中都有涉及。

上述问题并非无解,我们也可以通过HOOK的方式去解决。大致的思路是通过反射找到pending的队列,为其设置代理,使他永远返回一个空的列表。这样主线程就不会等待了。

2、MMKV的横空出世

MMKV的全称是什么?根据微信公众号的这篇文章,我猜应该是Memory Mapped Key Value。



2.1 MMKV的缘起

至少从一开始,MMKV的设计初衷就不是为了取代SP。根据官方的说法,微信客户端在日常运营中,时不时就会爆发特殊文字引起系统的 crash。为了发现这些引起闪退的异常文字,微信需要记录发生崩溃前的那一瞬间的环境,以便事后重新复现。

然而没有人知道崩溃什么时候会发生,这就要求实时写入信息。或者说,即使在程序崩溃后仍然能够有条不紊把信息记录下来。当时市面上的SharedPreferences、NSUserDefaults、SQLite 等常见组件都无法满足如此苛刻的性能要求,于是,微信团队自己实现了一套方案,也就是今天我们看到的mmkv。

mmkv最初是在ios上实现的,后来才扩展到了Android平台。可见,MMKV从一开始的设计初衷,就不是对标SharedPreference。



2.2 MMKV的实现原理

MMKV的实现原理在官方文档中都有说明,这里挑选一些重点简单介绍一下,有兴趣的同学可以进一步看官方的github。



2.2.1 内存映射

通过 mmap 内存映射文件,提供一段可供随时写入的内存块,App 只管往里面写数据,由操作系统负责将内存回写到文件,不必担心 crash 导致数据丢失。



2.2.2 数据组织

MMKV采用 protobuf 协议对数据进行序列化。但是标准 protobuf 不提供增量更新的能力,每次写入都必须全量写入。于是mmkv将增量 kv 对象序列化后,直接 append 到内存末尾。这样同一个 key 会有新旧若干份数据,最新的数据在最后。

使用 append 实现增量更新带来了一个新的问题,就是不断 append 的话,文件大小会增长得不可控。于是,mmkv又以内存 pagesize 为单位申请空间,在空间用尽之前都是 append 模式;当 append 到文件末尾时,进行文件重整、key 排重,尝试序列化保存排重结果;排重后空间还是不够用的话,将文件扩大一倍,直到空间足够。



2.2.3 数据有效性

考虑到文件系统、操作系统都有一定的不稳定性,mmkv增加了 crc 校验,对无效数据进行甄别。



2.2.4 支持多进程

MMKV使用了一系列复杂的架构来支持Android平台的多进程,包括去中心化架构的IPC,支持递归和锁升级/降级的文件锁等。

根据我的测试结果,MMKV在支持多进程的前提下,双进程密集写入,写入的耗时约为单进程的5~10倍。



2.3 MMKV存在的问题

如果我们使用SP同步写入文件,不可避免地会有IO操作。而IO本身的不稳定性使得耗时通常在ms级别,严重时甚至会导致用户掉帧。而MMKV通过 mmap 内存映射文件,只需要把数据写入内存,就几乎达到了同步写入文件的效果,即使进程崩溃也不会收到影响。从这一点上说,MMKV几乎是碾压SP的存在。但是,MMKV真的可以全方面替代SP吗?



2.3.1 与SP的apply()方法对比,MMKV并不占性能优势

如上文所述,通过MMKV把key-value写入内存,几乎就等于写入了文件。因此,MMKV对标的是SP的commit方法。但是,如果和apply相比呢?

我们使用下述的代码进行测试:

kotlin 复制代码
val times = 10_000

val testString = "sp_val_with_"
val spCostString = measureTimeMillis {
    val edit = sp.edit()
    for (i in 1..times) {
        edit.putString("sp_key_$i", "$testString $i")
    }
    edit.apply()
}

val mvCostString = measureTimeMillis {
    for (i in 1..times) {
        mmkv.encode("sp_key_$i", "$testString $i")
    }
}

Log.d("denny", "spCost: $spCostString ms, mmkvCost: $mvCostString ms")

在全新安装APP(无本地缓存)的前提下进行测试,测试机型为OPPO Reno10,Android版本11。最终得到的数据是:

D/denny: spCost: 36 ms, mmkvCost: 75 ms

也就是说,在异步写入的情况下,MMKV的耗时比SP还多。



2.3.2 MMKV在写入大数据时,性能下降严重

我们仅仅把上面的测试代码中的

val testString = "sp_val_with_"

修改为:

val testString = "sp_val_with_".repeat (10).repeat (10).repeat(10)

再次重新运行,得到的结果就变成了:

D/denny: spCost: 269 ms, mmkvCost: 1969 ms

在写入长字符串等大数据的情况下,MMKV的性能劣化比SP要严重得多。

猜测导致上述问题的原因是:MMKV通过mmap开辟的内存,是在native中完成的。每一次的写入操作,都需要一次从JNI到Native的内存拷贝。当写入数据变大时,这一次的内存拷贝的损耗就变得不可忽略了。



2.3.3 MMKV丢数据的问题

尽管MMKV的机制能够在进程退出后,继续完成把数据持久化的任务。这一点让MMKV看起来安心不少。但是需要注意的是,MMKV数据损坏的概率其实比SP要高。

如果设备在数据写入mmap开辟的内存中的过程中、对文件进行整理的过程中断电重启,那么MMKV将有很大概率出现数据损坏的情况。

而SP的机制则是在写入前,对原始文件进行备份。写入成功后删除备份文件。这种机制使得即使设备出现了意外,那么大概率也是丢失部分数据,而不是整个文件被写坏了。

在 iOS 微信现网环境上,平均有约 70万日次的数据校验不通过。需要注意,这只是2018年的数据。



3、SP的正统继任者:DataStore

如果说SP最大的问题是卡顿,那么MMKV和DataStore(下面简称DS)分别从两个不同的方向来解决这个问题。DS的解决方案,就是通过协程把同步的操作,变成异步的。

DS的API会返回一个Flow对象,通过这个Flow对象,你可以轻松地切换线程完成异步化操作,也可以监听数据的读写操作。

DataStore 提供了两种不同的实现:Preferences DataStore 和 Proto DataStore。前者对标的就是SP,而后者底层使用proto协议,可以保存写入的数据的类型(string/int/float等),这样就确保了类型安全。

不过需要注意的是,DS的API比SP和MMKV都要复杂得多。因此,在同样异步写入轻量级数据的过程中,DS的性能反而是最差的那一个。



4、总结

名称 SharedPreference MMKV DataStore
是否阻塞主线程 是 否。只需写入内存即可实现数据的持久化,耗时极短 否。通过协程实现异步化
是否支持跨进程 否 是 否
是否是类型安全的 否 否 是
是否能监听数据变化 否 否 是











5.附录:参考资料

  1. juejin.cn/post/688144...
  2. github.com/Tencent/MMK...
  3. blog.csdn.net/m0_66264910...
  4. www.jianshu.com/p/a2c1c92a2...
相关推荐
怕浪猫8 小时前
2026 年 AI Agent 面试到底考什么?这套题库覆盖了 90% 的高频考点
面试·aigc·ai编程
半摆烂日常10 小时前
低代码平台API对接实践:接口鉴权与数据同步的完整实现
android·低代码·rxjava
事圆则缓11 小时前
volatile 使用场景全解析:它能保证什么,又解决不了什么
android
泡沫_花火11 小时前
大模型开发相关岗位面试资料(含算法)
语言模型·自然语言处理·面试·职场和发展
蒲公英内测分发12 小时前
智能投影仪参加海外展会,未上架的 Android 配套 App 怎么让客户限时试用?
android
ii_best12 小时前
按键精灵手机端开发安卓版实战:手写一个可最小化、可拖动的「运行日志悬浮窗」(附完整 源码 + 踩坑记录)
android·ios·智能手机·自动化·ai编程·按键精灵
终端安全笔记12 小时前
安卓做 MDM 管控,先分清三种注册入口
android·安全·智能手机
乌萨达13 小时前
手机模拟器安卓怎么用?电脑安装应用、玩手游与账号同步指南
android·智能手机·电脑·雷电模拟器
美狐美颜SDK开放平台13 小时前
Android与iOS直播APP美颜有什么区别?视频美颜SDK开发详解
android·ios·音视频·视频美颜sdk