600字范文,内容丰富有趣,生活中的好帮手!
600字范文 > 【5年Android从零复盘系列之三十四】Android存储(9):腾讯MMKV 高性能键值存储组件详解

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

时间:2022-07-27 07:23:31

相关推荐

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

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

【代码是最好得老师】

1.概要

Github

官方文档

Demo.zip

MMKV 是基于 mmap 内存映射的 key-value 组件性能高,稳定性强(底层序列化/反序列化使用 protobuf 实现)支持加密支持多进程共享支持匿名内存,内存悬浮不落地文件,安全性极高效率极高支持SharedPreferences直接迁移支持类型: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;//修改默认加密keypublic 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 simplepublic 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()//mit();}

5.2 日志

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

@Overridepublic boolean wantLogRedirecting() {return true;}@Overridepublic 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 instanceMMKV kv = MMKV.mmkvWithID(mmapID, MMKV.SINGLE_PROCESS_MODE, null);// change from unencrypted to encryptedkv.reKey("Key_seq_1");// change encryption keykv.reKey("Key_seq_2");// change from encrypted to unencryptedkv.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接口,添加以下代码:

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

5.8 多进程与实现

多进程设计与实现

6.代码

Demo.zip

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。