MMKV实践与测试 - Xiasm/Java-Android-Learn GitHub Wiki

前言

微信最新开源项目MMKV开源当日即登Github T认定日榜,三日后登上周榜。MMKV是基于内存映射的移动端通用key-value组件,底层序列化反序列化使用protobuf实现,性能高,稳定性强。从2015年中至今,在IOS微信上使用已有近三年,近期移置到Android平台,移动端全平台通用,并全部在Github上开源。(来自官方介绍)

按顺序来,先说使用方式再说性能,最后谈谈它的原理和弊端。(其实官方文档介绍的已经非常清楚了,之所以写这篇博客,是自己做了下总结给懒人看的)

使用

使用方式很简单,作为如今的Android开发,我们只需要通过gradle引入如下:

implementation 'com.tencent:mmkv:1.0.10'

这里需要注意,MMKV支持的 Android minSdkVersion 为16,如果你的项目还在支持Android 4.0,那么就不建议使用了。
使用起来很简单,首先需要初始化MMKV:

MMKV.initialize(this);

然后获取MMKV实例:

MMKV mmkv = MMKV.defaultMMKV();

存储数据就更简单了,只需要调用如下,不用再如prefences一样调用apply或commit:

mmkv.encode("key", "value");

接着我们来获取下存储的信息:

String value = mmkv.decodeString("key", null);

好了,最简单的使用方式已经完了,可以看得出,MMKV的api操作相对SharePerfences来说非常简单,也省下了很多代码!
当然了,MMKV的使用方式还有很多,并且还支持SharePerfences迁移等,这里我直接附上官方的使用教程:
使用指南

性能测试

如果只是使用便捷,并不能打动我们去替换现有的存储方式,MMKV的性能才是值得尝试它的主要因素。官方文档里介绍了它的性能要比SharePerfences快非常多,作为一个严谨的码农,我们还是实际测试一下更为安全。下面看demo:

我们分别通过SharePerfences和MMKV存储一千条数据,然后对比下两者消耗的时间:

//使用SharePrefences存储一千条key-value
private void saveDataByPrefences() {
    new Thread(new Runnable() {
        @Override
        public void run() {
            Log.d(TAG, "run:prefences " + Thread.currentThread().getName());
            Debug.startMethodTracing("/sdcard/test_prefences");
            SharedPreferences preferences = getSharedPreferences("test", MODE_PRIVATE);
            SharedPreferences.Editor editor = preferences.edit();
            for (int i = 0; i < 1000; i++) {
                editor.putString(String.valueOf(i), String.valueOf(i));
            }
            editor.apply();
            Debug.stopMethodTracing();
        }
    }).start();
}

//使用MMKV存储一千条key-value
private void saveDataByMMKV() {
    new Thread(new Runnable() {
        @Override
        public void run() {
            Log.d(TAG, "run:mmkv " + Thread.currentThread().getName());
            Debug.startMethodTracing("/sdcard/test_mmkv");
            MMKV mmkv = MMKV.defaultMMKV();
            for (int i = 0; i < 1000; i++) {
                mmkv.encode(String.valueOf(i), String.valueOf(i));
            }
            Debug.stopMethodTracing();
        }
    }).start();
}

这里我们用Android的 debug trance 方式去测试两种存储消耗的时间,下面看图:



share prefences方式


mmkv方式

上面两图分别为在子线程用shareprefences存储一千条数据和用MMKV存储一千条数据的耗时情况,可以看出,这两中方式在子线程的耗时时间分别为1717183(us)和99637(us),整整相差两个数量级。所以说,在性能上MMKV是要比SharePerfences优越不少的。
无论是单进程还是多进程,操作MMKV都要比操作SharePrefences和SQLite快上很多倍,这里附上官方性能对比:

Android 性能对比

MMKV有哪些限制

要说MMKV的限制,就不得不了解它的存储方式。MMKV是通过 mmap 内存映射文件,提供一段可供随时写入的内存块,App 只管往里面写数据,由操作系统负责将内存回写到文件,不必担心 crash 导致数据丢失。下面是官方文档里的写入介绍:“MMKV在序列化的时候采用protobuf协议,标准 protobuf 不提供增量更新的能力,每次写入都必须全量写入。考虑到主要使用场景是频繁地进行写入更新,我们需要有增量更新的能力:将增量 kv 对象序列化后,直接 append 到内存末尾;这样同一个 key 会有新旧若干份数据,最新的数据在最后;那么只需在程序启动第一次打开 mmkv 时,不断用后读入的 value 替换之前的值,就可以保证数据是最新有效的。”,从这段话里我们知道了它的增量更新是通过直接将要更新的key-value插入内存末尾,而前面的并不会删除,除非达到了MMKV的内存限制才会进行重排,下面用一个简单的例子演示下更能明白:

//假设 lastActive 属性为统计某一页面的最后打开时间
mmkv.encode("lastActive", 200);

//然后用户不断地打开此页面,就会不断地更新lastActive属性,则MMKV在内存中就会存储多份实例

//假设这些key-value就是存储的数据
<key>lastActive</key><value>200</value>
<key>lastActive</key><value>300</value>
<key>lastActive</key><value>400</value>
    //...
<key>lastActive</key><value>1000</value>
<key>lastActive</key><value>2000</value>

可看到,一个键会存入多分实例,最后存入的就是最新的。
那么随之而来的就有另一个问题,就是不断存储的话,文件大小会增长得不可控。例如同一个 key 不断更新的话,是可能耗尽几百 M 甚至上 G 空间,而事实上整个 kv 文件就这一个 key,不到 1k 空间就存得下。这明显是不可取的。我们需要在性能和空间上做个折中:以内存 pagesize 为单位申请空间,在空间用尽之前都是 append 模式;当 append 到文件末尾时,进行文件重整、key 排重,尝试序列化保存排重结果;排重后空间还是不够用的话,将文件扩大一倍,直到空间足够。
所以MMKV的限制我们大家应该很明白了,MMKV 在大部分情况下都性能强劲,key/value 的数量和长度都没有限制。然而MMKV 在内存里缓存了所有的 key-value,在总大小比较大的情况下(例如 100M+),App 可能会爆内存,触发重整回写时,写入速度也会变慢。
MMKV的限制腾讯团队当然很清楚,也正在开发中,所以期待下一个大版本MMKV能有所优化。

⚠️ **GitHub.com Fallback** ⚠️