MMKV 概述

1. MMKV——基于 mmap 的高性能通用 key-value 组件

MMKV 是基于 mmap 内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。从 2015 年中至今在微信上使用,其性能和稳定性经过了时间的验证。GitHub地址:https://github.com/Tencent/MMKV

2. MMKV 原理

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

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

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

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

1. Android中使用

1.1 添加依赖

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

1.2 初始化

MMKV 的使用非常简单,所有变更立马生效,无需调用 syncapply。 在 App 启动时初始化 MMKV,设定 MMKV 的根目录(files/mmkv/),例如在 Application 里:

public class MyApp extends Application {private static final String TAG = MyApp.class.getSimpleName();@Overridepublic void onCreate() {super.onCreate();String rootDir = MMKV.initialize(this);Log.i(TAG,"mmkv root: " + rootDir);}
}

1.3 CRUD 操作

1)MMKV 提供一个全局的实例,可以直接使用:

import com.tencent.mmkv.MMKV;
...
MMKV kv = MMKV.defaultMMKV();kv.encode("bool", true);
System.out.println("bool: " + kv.decodeBool("bool"));kv.encode("int", Integer.MIN_VALUE);
System.out.println("int: " + kv.decodeInt("int"));kv.encode("long", Long.MAX_VALUE);
System.out.println("long: " + kv.decodeLong("long"));kv.encode("float", -3.14f);
System.out.println("float: " + kv.decodeFloat("float"));kv.encode("double", Double.MIN_VALUE);
System.out.println("double: " + kv.decodeDouble("double"));kv.encode("string", "Hello from mmkv");
System.out.println("string: " + kv.decodeString("string"));byte[] bytes = {'m', 'm', 'k', 'v'};
kv.encode("bytes", bytes);
System.out.println("bytes: " + new String(kv.decodeBytes("bytes")));

2)删除与查询

MMKV kv = MMKV.defaultMMKV();// 移除指定的key
kv.removeValueForKey("bool");
System.out.println("bool: " + kv.decodeBool("bool"));// 移除一组key
kv.removeValuesForKeys(new String[]{"int", "long"});
System.out.println("allKeys: " + Arrays.toString(kv.allKeys()));boolean hasBool = kv.containsKey("bool");

3)如果不同业务需要区别存储,也可以单独创建自己的实例:

MMKV mmkv = MMKV.mmkvWithID("MyID");
mmkv.encode("bool", true);

4)如果业务需要多进程访问,那么在初始化的时候加上标志位 MMKV.MULTI_PROCESS_MODE

MMKV mmkv = MMKV.mmkvWithID("InterProcessKV", MMKV.MULTI_PROCESS_MODE);
mmkv.encode("bool", true);

1.4 支持的数据类型

1)支持一下Java语言基础类型:

boolean、int、long、float、double、byte[]。

2)支持一下Java类和容器:

String、Set<String>、任何实现了Parcelable的类型。

1.5 SharedPreferences 迁移

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

2)MMKV 还额外实现了一遍 SharedPreferencesSharedPreferences.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();}

2. MMKV源码解析

本文基于MMKV1.2.2版本进行解析

2.1 初始化

当我们在使用MMKV之前,需要在Application中进行初始化,初始化方法上面有讲过,就是调用MMKV的initialize方法,代码如下所示:

    public static String initialize(Context context) {String root = context.getFilesDir().getAbsolutePath() + "/mmkv";// 日志级别MMKVLogLevel logLevel = MMKVLogLevel.LevelInfo;return initialize(root, (MMKV.LibLoader)null, logLevel);}

它使用的是内部存储空间下的mmkv文件夹作为根目录,然后调用 initialize 方法,代码如下:

    public static String initialize(String rootDir, MMKV.LibLoader loader, MMKVLogLevel logLevel) {if (loader != null) {if ("StaticCpp".equals("SharedCpp")) {loader.loadLibrary("c++_shared");}loader.loadLibrary("mmkv");} else {if ("StaticCpp".equals("SharedCpp")) {System.loadLibrary("c++_shared");}System.loadLibrary("mmkv");}MMKV.rootDir = rootDir;jniInitialize(MMKV.rootDir, logLevel2Int(logLevel)); // ... 1return rootDir;}

在注释1处调用 jniInitialize 这个native 方法进行 Native 层的初始化,代码如下所示:

MMKV_JNI void jniInitialize(JNIEnv *env, jobject obj, jstring rootDir, jint logLevel) {if (!rootDir) {return;}const char *kstr = env->GetStringUTFChars(rootDir, nullptr);if (kstr) {MMKV::initializeMMKV(kstr, (MMKVLogLevel) logLevel); // ... 1env->ReleaseStringUTFChars(rootDir, kstr);}
}

在注释1处调用 MMKV::initializeMMKV 对 MMKV 类进行了初始化,代码如下所示:

void MMKV::initializeMMKV(const MMKVPath_t &rootDir, MMKVLogLevel logLevel) {g_currentLogLevel = logLevel;ThreadLock::ThreadOnce(&once_control, initialize);g_rootDir = rootDir;mkPath(g_rootDir); // ... 1MMKVInfo("root dir: " MMKV_PATH_FORMAT, g_rootDir.c_str());
}

在注释1处通过mkPath函数创建对应的根目录。完成Native层的初始化工作。

2.2 获取MMKV对象

通过 mmkvWithID 方法可以获取 MMKV 对象,它传入的 mmapID 就对应了 SharedPreferences 中的 name,代表了一个文件对应的 name,而 rootPath 则对应了一个相对根目录的相对路径。

    @Nullablepublic static MMKV mmkvWithID(String mmapID, String rootPath) {if (rootDir == null) {throw new IllegalStateException("You should Call MMKV.initialize() first.");}long handle = getMMKVWithID(mmapID, SINGLE_PROCESS_MODE, null, rootPath);return checkProcessMode(handle, mmapID, SINGLE_PROCESS_MODE);}private static MMKV checkProcessMode(long handle, String mmapID, int mode) {if (handle == 0) {return null;}if (!checkedHandleSet.contains(handle)) {if (!checkProcessMode(handle)) {String message;if (mode == SINGLE_PROCESS_MODE) {message = "Opening a multi-process MMKV instance [" + mmapID + "] with SINGLE_PROCESS_MODE!";} else {message = "Opening a single-process MMKV instance [" + mmapID + "] with MULTI_PROCESS_MODE!";}throw new IllegalArgumentException(message);}checkedHandleSet.add(handle);}return new MMKV(handle);}

它调用到了 getMMKVWithId 这个 Native 方法,并获取到了一个 handle 变量, 然后通过 handle 构造了 Java 层的 MMKV 对象返回。这是一种很常见的手法,Java 层通过持有 Native 层对象的地址从而与 Native 对象通信(例如 Android 中的 Surface 就采用了这种方式)。getMMKVWithId  对应的Native方法代码如下所示:

MMKV_JNI jlong getMMKVWithID(JNIEnv *env, jobject, jstring mmapID, jint mode, jstring cryptKey, jstring rootPath) {MMKV *kv = nullptr;// mmapID 为 null 返回空指针if (!mmapID) {return (jlong) kv;}string str = jstring2string(env, mmapID);bool done = false;// 如果cryptKey不为null,则需要进行加密if (cryptKey) {// 获取加密的key,最后调用 MMKV::mmkvWithIDstring crypt = jstring2string(env, cryptKey);if (crypt.length() > 0) {if (rootPath) {string path = jstring2string(env, rootPath);kv = MMKV::mmkvWithID(str, DEFAULT_MMAP_SIZE, (MMKVMode) mode, &crypt, &path);} else {kv = MMKV::mmkvWithID(str, DEFAULT_MMAP_SIZE, (MMKVMode) mode, &crypt, nullptr);}done = true;}}// 如果不需要加密,则调用mmkvWithID不传入加密可以,表示不进行加密。if (!done) {if (rootPath) {string path = jstring2string(env, rootPath);kv = MMKV::mmkvWithID(str, DEFAULT_MMAP_SIZE, (MMKVMode) mode, nullptr, &path);} else {kv = MMKV::mmkvWithID(str, DEFAULT_MMAP_SIZE, (MMKVMode) mode, nullptr, nullptr);}}return (jlong) kv;
}

这里实际上调用了 MMKV::mmkvWithID 函数,它根据是否传入用于加密的 key 以及是否使用相对路径调用了不同的方法。

MMKV::mmkvWithID  函数代码如下所示:

MMKV *MMKV::mmkvWithID(const string &mmapID, int size, MMKVMode mode, string *cryptKey, string *rootPath) {if (mmapID.empty()) {return nullptr;}// 加锁SCOPED_LOCK(g_instanceLock);// 将mmapID 与 rootPath 结合生成 mmapKeyauto mmapKey = mmapedKVKey(mmapID, rootPath);// 通过 mmapKey 在 map 中查找对应的 MMKV 对象并返回auto itr = g_instanceDic->find(mmapKey);if (itr != g_instanceDic->end()) {MMKV *kv = itr->second;return kv;}// 如果不存在,则创建路径并构建MMKV对象并加入到 map 中。if (rootPath) {if (!isFileExist(*rootPath)) {if (!mkPath(*rootPath)) {return nullptr;}}MMKVInfo("prepare to load %s (id %s) from rootPath %s", mmapID.c_str(), mmapKey.c_str(), rootPath->c_str());}auto kv = new MMKV(mmapID, size, mode, cryptKey, rootPath);(*g_instanceDic)[mmapKey] = kv;return kv;
}

此函数的步骤如下所示:

1)通过 mmapedKVKey 方法对 mmapID 及 relativePath 进行结合生成了对应的 mmapKey,它会将它们两者的结合经过 md5 从而生成对应的 key,主要目的是为了支持不同相对路径下的同名 mmapID

2)通过 mmapKey 在 g_instanceDic 这个 map 中查找对应的 MMKV 对象,如果找到直接返回。

3)如果找不到对应的 MMKV 对象,构建一个新的 MMKV 对象,加入 map 后返回。

接下来我们分析 MMKV 的构造函数中做了什么,代码如下所示:

MMKV::MMKV(const string &mmapID, int size, MMKVMode mode, string *cryptKey, string *rootPath): m_mmapID(mmapedKVKey(mmapID, rootPath)) // historically Android mistakenly use mmapKey as mmapID, m_path(mappedKVPathWithID(m_mmapID, mode, rootPath)), m_crcPath(crcPathWithID(m_mmapID, mode, rootPath)), m_dic(nullptr), m_dicCrypt(nullptr), m_file(new MemoryFile(m_path, size, (mode & MMKV_ASHMEM) ? MMFILE_TYPE_ASHMEM : MMFILE_TYPE_FILE)), m_metaFile(new MemoryFile(m_crcPath, DEFAULT_MMAP_SIZE, m_file->m_fileType)), m_metaInfo(new MMKVMetaInfo()), m_crypter(nullptr), m_lock(new ThreadLock()), m_fileLock(new FileLock(m_metaFile->getFd(), (mode & MMKV_ASHMEM))), m_sharedProcessLock(new InterProcessLock(m_fileLock, SharedLockType)), m_exclusiveProcessLock(new InterProcessLock(m_fileLock, ExclusiveLockType)), m_isInterProcess((mode & MMKV_MULTI_PROCESS) != 0 || (mode & CONTEXT_MODE_MULTI_PROCESS) != 0) {m_actualSize = 0;m_output = nullptr;m_fileModeLock = nullptr;m_sharedProcessModeLock = nullptr;m_exclusiveProcessModeLock = nullptr;#    ifndef MMKV_DISABLE_CRYPT// 通过加密 key 构建 AES 加密对象 AESCryptif (cryptKey && cryptKey->length() > 0) {m_dicCrypt = new MMKVMapCrypt();m_crypter = new AESCrypt(cryptKey->data(), cryptKey->length());} else
#    endif{m_dic = new MMKVMap();}m_needLoadFromFile = true;m_hasFullWriteback = false;m_crcDigest = 0;m_sharedProcessLock->m_enable = m_isInterProcess;m_exclusiveProcessLock->m_enable = m_isInterProcess;// sensitive zone// 赋值操作// 加锁后调用 loadFromFile 加载数据{SCOPED_LOCK(m_sharedProcessLock);loadFromFile();}
}

这里进行了一些赋值操作,之后如果需要加密则根据用于加密的 cryptKey 生成对应的 AESCrypt 对象用于 AES 加密。最后,加锁后通过 loadFromFile 方法从文件中读取数据,这里的锁是一个跨进程的文件共享锁。接下来查看 loadFromFile函数,代码如下所示:

void MMKV::loadFromFile() {if (m_metaFile->isFileValid()) {m_metaInfo->read(m_metaFile->getMemory());}
#ifndef MMKV_DISABLE_CRYPTif (m_crypter) {if (m_metaInfo->m_version >= MMKVVersionRandomIV) {m_crypter->resetIV(m_metaInfo->m_vector, sizeof(m_metaInfo->m_vector));}}
#endifif (!m_file->isFileValid()) {m_file->reloadFromFile(); // ... 1}if (!m_file->isFileValid()) {MMKVError("file [%s] not valid", m_path.c_str());} else {// error checkingbool loadFromFile = false, needFullWriteback = false;// 对文件进行 CRC 校验,如果失败根据策略进行不同对处理checkDataValid(loadFromFile, needFullWriteback);MMKVInfo("loading [%s] with %zu actual size, file size %zu, InterProcess %d, meta info ""version:%u",m_mmapID.c_str(), m_actualSize, m_file->getFileSize(), m_isInterProcess, m_metaInfo->m_version);auto ptr = (uint8_t *) m_file->getMemory();// loading// // 从文件中读取内容if (loadFromFile && m_actualSize > 0) {MMKVInfo("loading [%s] with crc %u sequence %u version %u", m_mmapID.c_str(), m_metaInfo->m_crcDigest,m_metaInfo->m_sequence, m_metaInfo->m_version);// 创建 MMBuffer 对象,读取文件中的数据。MMBuffer inputBuffer(ptr + Fixed32Size, m_actualSize, MMBufferNoCopy);if (m_crypter) {clearDictionary(m_dicCrypt);} else {clearDictionary(m_dic);}if (needFullWriteback) {#ifndef MMKV_DISABLE_CRYPTif (m_crypter) {MiniPBCoder::greedyDecodeMap(*m_dicCrypt, inputBuffer, m_crypter);} else
#endif{MiniPBCoder::greedyDecodeMap(*m_dic, inputBuffer);}} else {#ifndef MMKV_DISABLE_CRYPTif (m_crypter) {MiniPBCoder::decodeMap(*m_dicCrypt, inputBuffer, m_crypter);} else
#endif{MiniPBCoder::decodeMap(*m_dic, inputBuffer);}}// 构造用于输出的 CodeOutputDatam_output = new CodedOutputData(ptr + Fixed32Size, m_file->getFileSize() - Fixed32Size);m_output->seek(m_actualSize);// 是否需要回写,将map中的数据写入到文件中。if (needFullWriteback) {fullWriteback();}} else {// file not valid or empty, discard everythingSCOPED_LOCK(m_exclusiveProcessLock);m_output = new CodedOutputData(ptr + Fixed32Size, m_file->getFileSize() - Fixed32Size);if (m_actualSize > 0) {writeActualSize(0, 0, nullptr, IncreaseSequence);sync(MMKV_SYNC);} else {writeActualSize(0, 0, nullptr, KeepSequence);}}auto count = m_crypter ? m_dicCrypt->size() : m_dic->size();MMKVInfo("loaded [%s] with %zu key-values", m_mmapID.c_str(), count);}m_needLoadFromFile = false;
}

我们先分析注释1处,如果文件不是有效的的,则需要调用 reloadFromFile 函数重新加载。代码如下所示:

void MemoryFile::reloadFromFile() {#    ifdef MMKV_ANDROIDif (m_fileType == MMFILE_TYPE_ASHMEM) {return;}
#    endifif (isFileValid()) {MMKVWarning("calling reloadFromFile while the cache [%s] is still valid", m_name.c_str());MMKV_ASSERT(0);clearMemoryCache();}// 打开对应的文件m_fd = open(m_name.c_str(), O_RDWR | O_CREAT | O_CLOEXEC, S_IRWXU);if (m_fd < 0) {MMKVError("fail to open:%s, %s", m_name.c_str(), strerror(errno));} else {FileLock fileLock(m_fd);InterProcessLock lock(&fileLock, ExclusiveLockType);SCOPED_LOCK(&lock);mmkv::getFileSize(m_fd, m_size);// round up to (n * pagesize)// 将文件大小对齐到页大小的整数倍,用 0 填充不足的部分if (m_size < DEFAULT_MMAP_SIZE || (m_size % DEFAULT_MMAP_SIZE != 0)) {size_t roundSize = ((m_size / DEFAULT_MMAP_SIZE) + 1) * DEFAULT_MMAP_SIZE;truncate(roundSize);} else {auto ret = mmap();if (!ret) {doCleanMemoryCache(true);}}
#    ifdef MMKV_IOStryResetFileProtection(m_name);
#    endif}
}

在 reloadFromFile 函数中 首先打开对应的文件,然后将文件大小对齐到页大小的整数倍,用 0 填充不足的部分,具体实现在 truncate 函数中完成,然后在调用 mmap 函数将文件映射到内存。

loadFromFile 函数的主要逻辑如下:

1)打开文件并获取文件大小,将文件的大小对齐到页的整数倍,不足则补 0(与内存映射的原理有关,内存映射是基于页的换入换出机制实现的)。

2)通过 mmap 函数将文件映射到内存中,然后通过  m_file->getMemory() 得到指向该区域的指针 ptr

3)对文件进行长度校验及 CRC 校验(循环冗余校验,可以校验文件完整性),在失败的情况下会根据当前策略进行抉择,如果策略是失败时恢复,则继续读取,并且在最后将 map 中的内容回写到文件。

4)通过 ptr 构造出一块用于管理 MMKV 映射内存的 MMBuffer 对象,如果需要解密,通过之前构造的 AESCrypt 进行解密。

5)由于 MMKV 使用了 protobuf 进行序列化,通过 MiniPBCoder::decodeMap 方法将 protobuf 转换成对应的 map。

6)构造用于输出的 CodedOutputData 类,如果需要回写(CRC 校验或文件长度校验失败),则调用 fullWriteback 方法将 map 中的数据回写到文件。

2.3 写入数据

Java 层的 MMKV 对象继承了 SharedPreferences 及 SharedPreferences.Editor 接口并实现了一系列如 putIntputLong 的方法用于对存储的数据进行修改,我们以 putInt 为例:

    @Overridepublic Editor putInt(String key, int value) {encodeInt(nativeHandle, key, value);return this;}

它调用到了 encodeInt 这个 Native 方法:

MMKV_JNI jboolean encodeInt(JNIEnv *env, jobject obj, jlong handle, jstring oKey, jint value) {MMKV *kv = reinterpret_cast<MMKV *>(handle);if (kv && oKey) {string key = jstring2string(env, oKey);return (jboolean) kv->set((int32_t) value, key);}return (jboolean) false;
}

这里将 Java 层持有的 NativeHandle 转为了对应的 MMKV 对象,之后调用了其 set 函数:

bool MMKV::set(int32_t value, MMKVKey_t key) {if (isKeyEmpty(key)) {return false;}size_t size = pbInt32Size(value);// 构造值对应的MMBuffer ,通过 CodeOutputData 将其写入 BufferMMBuffer data(size);CodedOutputData output(data.getPtr(), size);output.writeInt32(value);return setDataForKey(move(data), key);
}

set 函数首先获取到了写入的 value 在 protobuf 中所占据的大小,之后为其构造了对应的 MMBuffer 并将数据写入了这段 Buffer,最后调用到了 setDataForKey 函数,同时可以发现 CodedOutputData 是与 Buffer 交互的桥梁,可以通过它实现向 MMBuffer 中写入数据。

2.4 删除

通过 Java 层 MMKV 的 remove 方法可以实现删除操作:

    @Overridepublic Editor remove(String key) {removeValueForKey(key);return this;}

它调用了 removeValueForKey 这个 Native 方法:

MMKV_JNI void removeValueForKey(JNIEnv *env, jobject instance, jlong handle, jstring oKey) {// 通过java层的handle获取Native层的 MMKV对象指针。MMKV *kv = reinterpret_cast<MMKV *>(handle);if (kv && oKey) {string key = jstring2string(env, oKey);kv->removeValueForKey(key);}
}

调用了 Native 层 MMKV 的 removeValueForKey 函数:

void MMKV::removeValueForKey(MMKVKey_t key) {if (isKeyEmpty(key)) {return;}// 获取锁SCOPED_LOCK(m_lock);SCOPED_LOCK(m_exclusiveProcessLock);// 检查数据是否已经加载到了内存checkLoadData();removeDataForKey(key);
}

调用了 removeDataForKey 方法:

bool MMKV::removeDataForKey(MMKVKey_t key) {if (isKeyEmpty(key)) {return false;}
#ifndef MMKV_DISABLE_CRYPTif (m_crypter) {auto itr = m_dicCrypt->find(key);if (itr != m_dicCrypt->end()) {m_hasFullWriteback = false;static MMBuffer nan;
#    ifdef MMKV_APPLEauto ret = appendDataWithKey(nan, key, itr->second);if (ret.first) {auto oldKey = itr->first;m_dicCrypt->erase(itr);[oldKey release];}
#    elseauto ret = appendDataWithKey(nan, key);if (ret.first) {m_dicCrypt->erase(itr);}
#    endifreturn ret.first;}} else
#endif // MMKV_DISABLE_CRYPT{auto itr = m_dic->find(key);if (itr != m_dic->end()) {m_hasFullWriteback = false;static MMBuffer nan;auto ret = appendDataWithKey(nan, itr->second);if (ret.first) {#ifdef MMKV_APPLEauto oldKey = itr->first;m_dic->erase(itr);[oldKey release];
#elsem_dic->erase(itr);
#endif}return ret.first;}}return false;
}

这里实际上是构造了一条 size 为 0 的 MMBuffer 并调用 appendDataWithKey 将其 append 到 protobuf 文件中,并将 key 对应的内容从 map 中删除。读取时发现它的 size 为 0,则会认为这条数据已经删除。

2.5 读取数据

我们通过 getIntgetLong 等操作可以实现对数据的读取,我们以 getInt 为例:

    @Overridepublic int getInt(String key, int defValue) {return decodeInt(nativeHandle, key, defValue);}

它调用到了 decodeInt 这个 Native 方法:

MMKV_JNI jint decodeInt(JNIEnv *env, jobject obj, jlong handle, jstring oKey, jint defaultValue) {MMKV *kv = reinterpret_cast<MMKV *>(handle);if (kv && oKey) {string key = jstring2string(env, oKey);return (jint) kv->getInt32(key, defaultValue);}return defaultValue;
}

它调用到了 MMKV.getInt32ForKey 方法:

int32_t MMKV::getInt32(MMKVKey_t key, int32_t defaultValue) {if (isKeyEmpty(key)) {return defaultValue;}SCOPED_LOCK(m_lock);auto data = getDataForKey(key);if (data.length() > 0) {try {CodedInputData input(data.getPtr(), data.length());return input.readInt32();} catch (std::exception &exception) {MMKVError("%s", exception.what());}}return defaultValue;
}

它首先调用了 getDataForKey 方法获取到了 key 对应的 MMBuffer,之后通过 CodedInputData 将数据读出并返回。可以发现,长度为 0 时会将其视为不存在,返回默认值。

MMBuffer MMKV::getDataForKey(MMKVKey_t key) {checkLoadData();
#ifndef MMKV_DISABLE_CRYPTif (m_crypter) {auto itr = m_dicCrypt->find(key);if (itr != m_dicCrypt->end()) {auto basePtr = (uint8_t *) (m_file->getMemory()) + Fixed32Size;return itr->second.toMMBuffer(basePtr, m_crypter);}} else
#endif{auto itr = m_dic->find(key);if (itr != m_dic->end()) {auto basePtr = (uint8_t *) (m_file->getMemory()) + Fixed32Size;return itr->second.toMMBuffer(basePtr);}}MMBuffer nan;return nan;
}

这里实际上是通过在 Map 中寻找从而实现,找不到会返回 size 为 0 的 Buffer。

2.6 文件回写

MMKV 中,在一些特定的情景下,会通过 fullWriteback 函数立即将 map 的内容回写到文件。

回写时机主要有以下几个:

1)通过 MMKV.reKey 方法修改加密的 key。

2)删除一系列的 key 时(通过 removeValuesForKeys 方法)

3)读取文件时文件校验或 CRC 校验失败。

bool MMKV::fullWriteback(AESCrypt *newCrypter) {if (m_hasFullWriteback) {return true;}if (m_needLoadFromFile) {return true;}if (!isFileValid()) {MMKVWarning("[%s] file not valid", m_mmapID.c_str());return false;}// 如果  map 空了,直接清空文件if (m_crypter ? m_dicCrypt->empty() : m_dic->empty()) {clearAll();return true;}auto preparedData = m_crypter ? prepareEncode(*m_dicCrypt) : prepareEncode(*m_dic);auto sizeOfDic = preparedData.second;SCOPED_LOCK(m_exclusiveProcessLock);if (sizeOfDic > 0) {auto fileSize = m_file->getFileSize();if (sizeOfDic + Fixed32Size <= fileSize) {// 如果空间够写,直接写入return doFullWriteBack(move(preparedData), newCrypter);} else {assert(0);assert(newCrypter == nullptr);// ensureMemorySize will extend file & full rewrite, no need to write back again// 空间不够写入,调用 ensureMemorySize 进行扩容return ensureMemorySize(sizeOfDic + Fixed32Size - fileSize);}}return false;
}

这里首先在 map 为空的情况下,由于代表了所有数据已被删除,因此通过 clearAll 清除了文件与数据。否则它会对当前映射空间是否足够写入 map 中回写的数据,如果足够则会将数据写入,否则会调用 ensureMemorySize 从而进行内存重整与扩容。

转载:https://blog.csdn.net/lixiong0713/article/details/107998965

MMKV基本使用与源码解析相关推荐

  1. 谷歌BERT预训练源码解析(二):模型构建

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog.csdn.net/weixin_39470744/arti ...

  2. 谷歌BERT预训练源码解析(三):训练过程

    目录 前言 源码解析 主函数 自定义模型 遮蔽词预测 下一句预测 规范化数据集 前言 本部分介绍BERT训练过程,BERT模型训练过程是在自己的TPU上进行的,这部分我没做过研究所以不做深入探讨.BE ...

  3. 谷歌BERT预训练源码解析(一):训练数据生成

    目录 预训练源码结构简介 输入输出 源码解析 参数 主函数 创建训练实例 下一句预测&实例生成 随机遮蔽 输出 结果一览 预训练源码结构简介 关于BERT,简单来说,它是一个基于Transfo ...

  4. Gin源码解析和例子——中间件(middleware)

    在<Gin源码解析和例子--路由>一文中,我们已经初识中间件.本文将继续探讨这个技术.(转载请指明出于breaksoftware的csdn博客) Gin的中间件,本质是一个匿名回调函数.这 ...

  5. Colly源码解析——结合例子分析底层实现

    通过<Colly源码解析--框架>分析,我们可以知道Colly执行的主要流程.本文将结合http://go-colly.org上的例子分析一些高级设置的底层实现.(转载请指明出于break ...

  6. libev源码解析——定时器监视器和组织形式

    我们先看下定时器监视器的数据结构.(转载请指明出于breaksoftware的csdn博客) /* invoked after a specific time, repeatable (based o ...

  7. libev源码解析——定时器原理

    本文将回答<libev源码解析--I/O模型>中抛出的两个问题.(转载请指明出于breaksoftware的csdn博客) 对于问题1:为什么backend_poll函数需要指定超时?我们 ...

  8. libev源码解析——I/O模型

    在<libev源码解析--总览>一文中,我们介绍过,libev是一个基于事件的循环库.本文将介绍其和事件及循环之间的关系.(转载请指明出于breaksoftware的csdn博客) 目前i ...

  9. libev源码解析——调度策略

    在<libev源码解析--监视器(watcher)结构和组织形式>中介绍过,监视器分为[2,-2]区间5个等级的优先级.等级为2的监视器最高优,然后依次递减.不区分监视器类型和关联的文件描 ...

最新文章

  1. Computer OS系统基本原理
  2. 规格选项表管理之查询获取规格选项表列表数据
  3. Java性能调优、LinkedIn容器部署、阿里移动性能调优——首届APMCon精彩演讲先睹为快...
  4. matlab图像定位分割,車牌定位matlab程序:通過hsv彩色分割方式定位車牌
  5. 数据挖掘系列(4)使用weka做关联规则挖掘
  6. python爬取数据存入mysql
  7. 【数据竞赛】2020腾讯广告算法大赛冠军方案分享及代码
  8. 这个网盘搜索好像还不错
  9. DCMTK:查询/检索服务类用户(C-FIND操作)
  10. CVPR 2019 | 腾讯AI Lab解读六大前沿方向及33篇入选论文
  11. Another approach to enable table edit in SE16
  12. python调用ping命令_Windows 中通过Python实现ping命令加时间戳
  13. Java类class isAnnotationPresent()方法与示例
  14. 中国地质大学网络计算机考试试题,2017年中国地质大学(武汉)计算机学院830计算机软件综合之计算机网络考研题库...
  15. 技巧:如何提高git下载速度
  16. 2014蓝桥杯C++A组——史丰收速算
  17. Python中的切片(Slice)操作详解
  18. 【paddlepaddle安装报错系列】Installing collected packages :OpenCV-python ERROR :After october 2020 you…
  19. 日天的终生大事(dp)
  20. pix2code:Generating Code from a Graphical User Interface Screenshot

热门文章

  1. 移动端浏览器横屏、竖屏样式
  2. NeatUpload 大文件上传
  3. java如何加密_Java如何实现密码加密
  4. Charles功能介绍和使用教程
  5. 适用于 Windows 10的10款PDF编辑器评测
  6. 如何利用亚马逊关键词进行引流?权威关键词设置技巧来了
  7. stm32实现json格式传输/ cjson使用
  8. Free Spire.XLS for .NET 8.3 (社区版)
  9. 一款数据可视化分析报表工具
  10. USTC算法设计与分析-总结