【5年Android从零复盘系列之三十四】Android存储(9):腾讯MMKV 高性能键值存储组件详解


【代码是最好得老师】

1.概要

Github

官方文档

Demo.zip

  1. MMKV 是基于 mmap 内存映射的 key-value 组件
  2. 性能高,稳定性强(底层序列化/反序列化使用 protobuf 实现)
  3. 支持加密
  4. 支持多进程共享
  5. 支持匿名内存,内存悬浮不落地文件,安全性极高
  6. 效率极高
  7. 支持SharedPreferences直接迁移
  8. 支持类型:boolean、int、long、float、double、byte[]、String、Set、Parcelable

2.对比&原理

数据来源腾讯官方测试数据

2.1 单进程性能

可以直观看出MMKV性能秒杀SP、SQLite

(测试机器是 华为 Mate 20 Pro 128G,Android 10,每组操作重复 1k 次,时间单位是 ms。)

2.2 多进程性能

亦可看出,多进程中,MMKV都远远超越 MultiProcessSharedPreferences & SQLite & SQLite.
MMKV 在 Android 多进程 key-value 存储组件上是不二之选

(测试机器是 华为 Mate 20 Pro 128G,Android 10,每组操作重复 1k 次,时间单位是 ms。)

2.3 原理

  • 内存准备

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

  • 数据组织

数据序列化方面我们选用 protobuf 协议,pb 在性能和空间占用上都有不错的表现。

  • 写入优化

考虑到主要使用场景是频繁地进行写入更新,我们需要有增量更新的能力。我们考虑将增量 kv 对象序列化后,append 到内存末尾。

  • 空间增长

使用 append 实现增量更新带来了一个新的问题,就是不断 append 的话,文件大小会增长得不可控。我们需要在性能和空间上做个折中。

更多请移步MMKV原理

3.简单使用

1.module的build.gradle导包

dependencies {implementation 'com.tencent:mmkv-static:1.2.7'
}

2.app的Application中初始化,也可选择自定义自定义路径初始化MMKV,见Demo封装的工具类

public void onCreate() {super.onCreate();//最简单方式String rootDir = MMKV.initialize(this);
}

3.简单的使用:存取数据

MMKV kv = MMKV.defaultMMKV();kv.encode("bool", true);
boolean bValue = kv.decodeBool("bool");kv.encode("int", Integer.MIN_VALUE);
int iValue = kv.decodeInt("int");kv.encode("string", "Hello from mmkv");
String str = kv.decodeString("string");

4.高阶用法

同理,先导包

dependencies {implementation 'com.tencent:mmkv-static:1.2.7'
}

4.1.初始化(日志、多进程、加密、分组存储)

在app的Application中初始化,也可选择自定义自定义路径初始化MMKV,见Demo封装的工具类

public void onCreate() {super.onCreate();//进阶方式1MMKVHelper.init(this);//进阶方式2String dir = getFilesDir().getAbsolutePath() + "/mmkv_2";String rootDir = MMKVHelper.init(dir);
}

MMKVHelper封装类.方法,涵盖加密、多进程使用情形

public static  String ENCRPT_KEY = BuildConfig.LIBRARY_PACKAGE_NAME;//修改默认加密key
public static  String ENCRPT_KEY_MULTI_PROGRESS = BuildConfig.LIBRARY_PACKAGE_NAME;//修改默认加密key2public static String init(Application context) {return init(context ,"",true ,ENCRPT_KEY);
}public static String init(String mmkvFilePath) {return init(null,mmkvFilePath ,true ,ENCRPT_KEY);
}public static String init(Application context, String path ,boolean openLog ,String encryptKey) {String rootDir ;if (TextUtils.isEmpty(path)){rootDir = MMKV.initialize(context);}else {rootDir = MMKV.initialize(path);}if (openLog) {Log.d("MMKV root dir:", rootDir);MMKV.setLogLevel(MMKVLogLevel.LevelInfo);}else {//除非有非常强烈的证据表明MMKV的日志拖慢了App的速度,否则不应该关掉日志MMKV.setLogLevel(MMKVLogLevel.LevelNone);}if (!TextUtils.isEmpty(encryptKey)){ENCRPT_KEY = encryptKey;ENCRPT_KEY_MULTI_PROGRESS = encryptKey;}return rootDir;
}

4.2.增删改查

简单使用MMKV,默认配置都是单进程、不加密,封装类MMKVHelper进阶使用,开放便捷设置情况,完整代码见Demo封装类代码。

example:

4.2.1 存储(增、改):

public static boolean put(String key, String value) {return MMKV.defaultMMKV(MMKV.SINGLE_PROCESS_MODE , ENCRPT_KEY).encode(key, value);
}public static boolean put(String key, int value) {return MMKV.defaultMMKV(MMKV.SINGLE_PROCESS_MODE, ENCRPT_KEY).encode(key, value);
}//其他类型同理......public static Boolean put2Group(String GroupId, String key, Object value) {return put2Group(GroupId, key, value, false);
}//进阶
public static Boolean put2Group(String GroupId, String key, Object value, boolean multiProgress) {MMKV mmkv;if (multiProgress) {//如果业务需要多进程访问,那么在初始化的时候加上标志位 MMKV.MULTI_PROCESS_MODEmmkv = MMKV.mmkvWithID(GroupId, MMKV.MULTI_PROCESS_MODE, ENCRPT_KEY_MULTI_PROGRESS);} else {//默认单进程mmkv = MMKV.mmkvWithID(GroupId, MMKV.SINGLE_PROCESS_MODE, ENCRPT_KEY);}boolean flag = false;if (value instanceof Boolean) {flag = mmkv.encode(key, (Boolean) value);}if (value instanceof Integer) {flag = mmkv.encode(key, (int) value);}if (value instanceof Float) {flag = mmkv.encode(key, (Float) value);}if (value instanceof Double) {flag = mmkv.encode(key, (Double) value);}if (value instanceof Long) {flag = mmkv.encode(key, (Long) value);}if (value instanceof String) {flag = mmkv.encode(key, (String) value);}if (value instanceof Parcelable) {flag = mmkv.encode(key, (Parcelable) value);}return flag;
}

4.2.2 删除

 //delete simple
public static boolean delete(String deleteItemKey) {MMKV.defaultMMKV(MMKV.SINGLE_PROCESS_MODE, ENCRPT_KEY).remove(deleteItemKey);return MMKV.defaultMMKV(MMKV.SINGLE_PROCESS_MODE, ENCRPT_KEY).contains(deleteItemKey);
}public static boolean delete(String groupId, String deleteItemKey) {return delete(groupId, deleteItemKey, false);
}public static boolean delete(String groupId, String deleteItemKey, boolean multiProgress) {int mode = multiProgress ? MMKV.MULTI_PROCESS_MODE : MMKV.SINGLE_PROCESS_MODE;MMKV mmkv;if (mode == MMKV.MULTI_PROCESS_MODE) {mmkv = MMKV.mmkvWithID(groupId, MMKV.MULTI_PROCESS_MODE, ENCRPT_KEY_MULTI_PROGRESS);} else {mmkv = MMKV.mmkvWithID(groupId, mode, ENCRPT_KEY);}mmkv.remove(deleteItemKey);return mmkv.contains(deleteItemKey);
}

4.2.3 获取(查询)

基础获取、多进程获取、分组获取

public static String get(String key, String defValue) {return MMKV.defaultMMKV(MMKV.SINGLE_PROCESS_MODE , ENCRPT_KEY).decodeString(key, defValue);
}public static int get(String key, int defValue) {return MMKV.defaultMMKV(MMKV.SINGLE_PROCESS_MODE, ENCRPT_KEY).decodeInt(key, defValue);
}public static <T> T getByGroup(String GroupId, String key, Object defValue) {return (T) getByGroup(GroupId, key, defValue, false);
}public static Object getByGroup(String GroupId, String key, Object defValue, boolean multiProgress) {MMKV mmkv;if (multiProgress) {//如果业务需要多进程访问,那么在初始化的时候加上标志位 MMKV.MULTI_PROCESS_MODEmmkv = MMKV.mmkvWithID(GroupId, MMKV.MULTI_PROCESS_MODE, ENCRPT_KEY_MULTI_PROGRESS);} else {//默认单进程mmkv = MMKV.mmkvWithID(GroupId, MMKV.SINGLE_PROCESS_MODE, ENCRPT_KEY);}if (defValue instanceof Boolean) {return mmkv.decodeBool(key, (Boolean) defValue);}if (defValue instanceof Integer) {return mmkv.decodeInt(key, (int) defValue);}if (defValue instanceof Float) {return mmkv.decodeFloat(key, (Float) defValue);}if (defValue instanceof Double) {return mmkv.decodeDouble(key, (Double) defValue);}if (defValue instanceof Long) {return mmkv.decodeLong(key, (Long) defValue);}if (defValue instanceof String) {return mmkv.decodeString(key, (String) defValue);}if (defValue instanceof Parcelable) {return mmkv.decodeParcelable(key, (Class<Parcelable>) defValue);}return null;
}

5.其他设置

5.1 SP迁移

SharedPreferences 迁移
MMKV 提供了 importFromSharedPreferences() 函数,可以比较方便地迁移数据过来。

MMKV 还额外实现了一遍 SharedPreferences、SharedPreferences.Editor 这两个 interface,在迁移的时候只需两三行代码即可,其他 CRUD 操作代码都不用改。

private void testImportSharedPreferences() {//SharedPreferences preferences = getSharedPreferences("myData", MODE_PRIVATE);MMKV preferences = MMKV.mmkvWithID("myData");// 迁移旧数据{SharedPreferences old_man = getSharedPreferences("myData", MODE_PRIVATE);preferences.importFromSharedPreferences(old_man);old_man.edit().clear().commit();}// 跟以前用法一样SharedPreferences.Editor editor = preferences.edit();editor.putBoolean("bool", true);editor.putInt("int", Integer.MIN_VALUE);editor.putLong("long", Long.MAX_VALUE);editor.putFloat("float", -3.14f);editor.putString("string", "hello, imported");HashSet<String> set = new HashSet<String>();set.add("W"); set.add("e"); set.add("C"); set.add("h"); set.add("a"); set.add("t");editor.putStringSet("string-set", set);// 无需调用 commit()//editor.commit();
}

5.2 日志

MMKV 默认将日志打印到 logcat,不便于对线上问题进行定位和解决。你可以在 App 启动时接收转发 MMKV 的日志。实现MMKVHandler接口,添加类似下面的代码:

@Override
public boolean wantLogRedirecting() {return true;
}@Override
public void mmkvLog(MMKVLogLevel level, String file, int line, String func, String message) {String log = "<" + file + ":" + line + "::" + func + "> " + message;switch (level) {case LevelDebug://Log.d("redirect logging MMKV", log);break;case LevelInfo://Log.i("redirect logging MMKV", log);break;case LevelWarning://Log.w("redirect logging MMKV", log);break;case LevelError://Log.e("redirect logging MMKV", log);break;case LevelNone://Log.e("redirect logging MMKV", log);break;}
}

日志组件推荐使用 xlog,同样也是开源自微信团队。

关闭日志(不建议):

除非有非常强烈的证据表明MMKV的日志拖慢了App的速度,否则不应该关掉日志。没有日志,日后万一用户有问题,将无法跟进。

MMKV.setLogLevel(MMKVLogLevel.LevelNone);//关闭日志

5.3 加密

MMKV 默认明文存储所有 key-value,依赖 Android 系统的沙盒机制保证文件加密。如果你担心信息泄露,你可以选择加密 MMKV。

String cryptKey = "My-Encrypt-Key";
MMKV kv = MMKV.mmkvWithID("MyID", MMKV.SINGLE_PROCESS_MODE, cryptKey);
你可以更改密钥,也可以将一个加密 MMKV 改成明文,或者反过来。final String mmapID = "testAES_reKey1";
// an unencrypted MMKV instance
MMKV kv = MMKV.mmkvWithID(mmapID, MMKV.SINGLE_PROCESS_MODE, null);// change from unencrypted to encrypted
kv.reKey("Key_seq_1");// change encryption key
kv.reKey("Key_seq_2");// change from encrypted to unencrypted
kv.reKey(null);

5.4 自定义根目录

MMKV 默认把文件存放在$(FilesDir)/mmkv/目录。你可以在 App 启动时自定义根目录:

String dir = getFilesDir().getAbsolutePath() + "/mmkv_2";
String rootDir = MMKV.initialize(dir);
Log.i("MMKV", "mmkv root: " + rootDir);
MMKV 甚至支持自定义某个文件的目录:String relativePath = getFilesDir().getAbsolutePath() + "/mmkv_3";
MMKV kv = MMKV.mmkvWithID("testCustomDir", relativePath);

注意:官方推荐将 MMKV 文件存储在你 App 的私有路径内部,不要 存储在 external storage(也就是 SD card)。

5.5 自定义 library loader

一些 Android 设备(API level 19)在安装/更新 APK 时可能出错, 导致 libmmkv.so 找不到。然后就会遇到 java.lang.UnsatisfiedLinkError 之类的 crash。有个开源库 ReLinker 专门解决这个问题,你可以用它来加载 MMKV:

String dir = getFilesDir().getAbsolutePath() + "/mmkv";
MMKV.initialize(dir, new MMKV.LibLoader() {@Overridepublic void loadLibrary(String libName) {ReLinker.loadLibrary(MyApplication.this, libName);}
});

5.6 Native Buffer

当从 MMKV 取一个 String or byte[]的时候,会有一次从 native 到 JVM 的内存拷贝。如果这个值立即传递到另一个 native 库(JNI),又会有一次从 JVM 到 native 的内存拷贝。当这个值比较大的时候,整个过程会非常浪费。Native Buffer 就是为了解决这个问题。
Native Buffer 是由 native 创建的内存缓冲区,在 Java 里封装成 NativeBuffer 类型,可以透明传递到另一个 native 库进行访问处理。整个过程避免了先拷内存到 JVM 又从 JVM 拷回来导致的浪费。示例代码:

int sizeNeeded = kv.getValueActualSize("bytes");
NativeBuffer nativeBuffer = MMKV.createNativeBuffer(sizeNeeded);
if (nativeBuffer != null) {int size = kv.writeValueToNativeBuffer("bytes", nativeBuffer);Log.i("MMKV", "size Needed = " + sizeNeeded + " written size = " + size);// pass nativeBuffer to another native library// ...// destroy when you're doneMMKV.destroyNativeBuffer(nativeBuffer);
}

5.7 数据恢复

在 crc 校验失败,或者文件长度不对的时候,MMKV 默认会丢弃所有数据。你可以让 MMKV 恢复数据。要注意的是修复率无法保证,而且可能修复出奇怪的 key-value。同样地也是实现MMKVHandler接口,添加以下代码:

@Override
public MMKVRecoverStrategic onMMKVCRCCheckFail(String mmapID) {return MMKVRecoverStrategic.OnErrorRecover;
}@Override
public MMKVRecoverStrategic onMMKVFileLengthError(String mmapID) {return MMKVRecoverStrategic.OnErrorRecover;
}

5.8 多进程与实现

多进程设计与实现

6.代码

Demo.zip

【5年Android从零复盘系列之三十四】Android存储(9):腾讯MMKV 高性能键值存储组件详解相关推荐

  1. 【5年Android从零复盘系列之十七】Android自定义View(12):手势绘制及GestureOverlayView事件详解(图文)

    [5年Android从零复盘系列之十七]Android自定义View(12):手势绘制及GestureOverlayView事件浅析 1.基础 掌握View体系事件分发与处理,参考Android自定义 ...

  2. [Python从零到壹] 六十四.图像识别及经典案例篇之图像傅里叶变换和傅里叶逆变换详解

    祝大家新年快乐,阖家幸福,健康快乐! 欢迎大家来到"Python从零到壹",在这里我将分享约200篇Python系列文章,带大家一起去学习和玩耍,看看Python这个有趣的世界.所 ...

  3. 【5年Android从零复盘系列之二十】Android自定义View(15):Matrix详解(图文)【转载】

    [转载]本文转载自麻花儿wt 的文章<android matrix 最全方法详解与进阶(完整篇)> [5年Android从零复盘系列之二十]Android自定义View(15):Matri ...

  4. 【Android从零单排系列十一】《Android视图控件——日历、日期、时间选择控件》

    目录 一.日历.日期.时间组件基本介绍 二.几种常见的控件类型 1.CalendarView –日历控件 2. DatePicker –日期选择控件 3.TimePicker –时间选择控件 4.Ch ...

  5. 【Android从零单排系列一】《Android系统发展史》

    目录 前言 一.Android 创始人 二.Android 发展历程 三.Android各版本发布时间及特性 1.AndroidBeta:阿童木 2.Android 1.0:发条机器人 3.Andro ...

  6. 应用安全系列之三十四:数值溢出

    应用程序中难免会遇到数字的处理,针对数字的处理如果不当也会造成严重的问题,著名的Heartbleed漏洞也是没有验证数字的有效性导致的. 数值处理不好的,轻则产生异常,重则影响整个程序的正常运行,因此 ...

  7. 【5年Android从零复盘系列之六】Android自定义View(1):基础详解(图文)

    1.基础一:坐标计算 1.1 Android窗口坐标系计算以屏幕左上角为原点, 向右为X轴正向,向下为Y轴正向 1.2 View坐标系 [注意获取的坐标是像素值,不是dp值] [注意获取的坐标是像素值 ...

  8. 《神探tcpdump第五招》-linux命令五分钟系列之三十九

    == [系列文章] <神探tcpdump第一招>-linux命令五分钟系列之三十五 <神探tcpdump第二招>-linux命令五分钟系列之三十六 <神探tcpdump第 ...

  9. Android应用开发提高系列(5)——Android动态加载(下)——加载已安装APK中的类和资源...

    前言  Android动态加载(下)--加载已安装APK中的类和资源. 声明 欢迎转载,但请保留文章原始出处:)  博客园:http://www.cnblogs.com 农民伯伯: http://ov ...

  10. Debezium报错处理系列之三十六:Task threw an uncaught and unrecoverable exception. Task is being killed and will

    Debezium报错处理系列之三十六:Task threw an uncaught and unrecoverable exception. Task is being killed and will ...

最新文章

  1. ArchSummit2018深圳站筹备中,18大专题征集演讲嘉宾
  2. 显示 grep 结果的指定行
  3. mysql 使用索引_mysql使用索引的注意事项
  4. Unparsed aapt error(s)! Check the console for output
  5. Win10 JAVA安装及环境搭建(windows jdk,windows java环境配置)
  6. 打印工资条怎么做到每个人都有表头明细_一分钟生成500人的工资条?还有2种方法?...
  7. 学会使用JDK API
  8. ctk介绍、安装、使用详细说明pdf文档(中文).rar_Minio 安装和使用详解,还有对.net api进行了二次封装...
  9. NUC1196 Sum【水题+数学题】
  10. 从入坑到入门 | 龙蜥开发者说第2期
  11. 卸下重负,苏宁易购重组价值逻辑
  12. 发票ocr识别查验解决方案
  13. 逆火效应:该对谁负责?
  14. Linux压缩命令gzip, bzip2和tar
  15. Hello World---kodu少儿编程第四天
  16. Qt quick 示例:推特搜索小程序开发
  17. 人工智能处理的几大基础任务、元宇宙和人工智能
  18. 微信电脑版无法获取二维码
  19. 将mqm用户的环境变量写入配置文件
  20. 数字图像处理——加权均值滤波器

热门文章

  1. Moodle插件开发基础
  2. AI语音克隆软件安装和使用教程
  3. 【系统分析师之路】2011年系统分析师上午综合知识真题
  4. WINDOWS XP下驱动开发环境设置(DDK+VC6.0)
  5. 虚幻引擎3控制台命令
  6. PDF417条码生成类
  7. android抢qq红包源码,QQ抢红包插件实现
  8. android win10驱动安装失败,小米手机驱动程序在win10系统中安装失败的解决方案
  9. iOS 蓝牙开发实现文件传输
  10. Maven开发笔记(三)—— Maven中dependencies和dependencyManagement