|
|
|
|
公众号矩阵

源码进阶:腾讯开源轻量级缓存 Mmkv 源码解析

MMKV 是基于 mmap 内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。

作者: Android开发编程来源: Android开发编程|2021-10-20 07:18

本文转载自微信公众号「Android开发编程」,作者Android开发编程。转载本文请联系Android开发编程公众号。

前言

MMKV本质上的定位和sp有点相似,经常用于持久化小数据的键值对;

其速度可以说是当前所有同类型中速度最快,性能最优的库;

今天我们就来聊聊;

一、MMKV介绍和简单使用

1、什么是mmkv

MMKV 是基于 mmap 内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强;

MMKV 基本原理

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

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

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

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

2、MMKV的使用

使用前请初始化:

  1. MMKV.initialize(this) 

mmkv写入键值对;

  1. var mmkv = MMKV.defaultMMKV() 
  2. mmkv.encode("bool",true
  3. mmkv.encode("int",1) 
  4. mmkv.encode("String","test"
  5. mmkv.encode("float",1.0f) 
  6. mmkv.encode("double",1.0) 

mmkv除了能够写入这些基本类型,只要SharePrefences支持的,它也一定能够支持;

mmkv读取键值对;

  1. var mmkv = MMKV.defaultMMKV() 
  2. var bo = mmkv.decodeBool("bool"
  3. Log.e(TAG,"bool:${bo}"
  4. var i = mmkv.decodeInt("int"
  5. Log.e(TAG,"int:${i}"
  6. var s = mmkv.decodeString("String"
  7. Log.e(TAG,"String:${s}"
  8. var f = mmkv.decodeFloat("float"
  9. Log.e(TAG,"float:${f}"
  10. var d = mmkv.decodeDouble("double"
  11. Log.e(TAG,"double:${d}"

每一个key读取的数据类型就是decodexxx对应的类型名字;

mmkv 删除键值对和查键值对;

  1. var mmkv = MMKV.defaultMMKV() 
  2. mmkv.removeValueForKey("String"
  3. mmkv.removeValuesForKeys(arrayOf("int","bool")) 
  4. mmkv.containsKey("String"

能够删除单个key对应的value,也能删除多个key分别对应的value;

containsKey判断mmkv的磁盘缓存中是否存在对应的key;

二、MMKV 源码解析

1、初始化

通过 MMKV.initialize 方法可以实现 MMKV 的初始化:

  1. public static String initialize(Context context) { 
  2.     String root = context.getFilesDir().getAbsolutePath() + "/mmkv"
  3.     return initialize(root); 

它采用了内部存储空间下的 mmkv 文件夹作为根目录,之后调用了 initialize 方法;

  1. public static String initialize(String rootDir) { 
  2.     MMKV.rootDir = rootDir; 
  3.     jniInitialize(MMKV.rootDir); 
  4.     return rootDir; 

调用到了 jniInitialize 这个 Native 方法进行 Native 层的初始化:

  1. extern "C" JNIEXPORT JNICALL void 
  2. Java_com_tencent_mmkv_MMKV_jniInitialize(JNIEnv *env, jobject obj, jstring rootDir) { 
  3.     if (!rootDir) { 
  4.         return
  5.     } 
  6.     const char *kstr = env->GetStringUTFChars(rootDir, nullptr); 
  7.     if (kstr) { 
  8.         MMKV::initializeMMKV(kstr); 
  9.         env->ReleaseStringUTFChars(rootDir, kstr); 
  10.     } 

这里通过 MMKV::initializeMMKV 对 MMKV 类进行了初始化:

  1. void MMKV::initializeMMKV(const std::string &rootDir) { 
  2.     static pthread_once_t once_control = PTHREAD_ONCE_INIT; 
  3.     pthread_once(&once_control, initialize); 
  4.     g_rootDir = rootDir; 
  5.     char *path = strdup(g_rootDir.c_str()); 
  6.     mkPath(path); 
  7.     free(path); 
  8.     MMKVInfo("root dir: %s", g_rootDir.c_str()); 

实际上就是记录下了 rootDir 并创建对应的根目录,由于 mkPath 方法创建目录时会修改字符串的内容,因此需要复制一份字符串进行;

2、获取 MMKV 对象

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

  1. @Nullable 
  2. public static MMKV mmkvWithID(String mmapID, String relativePath) { 
  3.     if (rootDir == null) { 
  4.         throw new IllegalStateException("You should Call MMKV.initialize() first."); 
  5.     } 
  6.     long handle = getMMKVWithID(mmapID, SINGLE_PROCESS_MODE, null, relativePath); 
  7.     if (handle == 0) { 
  8.         return null
  9.     } 
  10.     return new MMKV(handle); 

它调用到了 getMMKVWithId 这个 Native 方法,并获取到了一个 handle 构造了 Java 层的 MMKV 对象返回;

Java 层通过持有 Native 层对象的地址从而与 Native 对象通信;

  1. extern "C" JNIEXPORT JNICALL jlong Java_com_tencent_mmkv_MMKV_getMMKVWithID( 
  2.     JNIEnv *env, jobject obj, jstring mmapID, jint mode, jstring cryptKey, jstring relativePath) { 
  3.     MMKV *kv = nullptr; 
  4.       // mmapID 为 null 返回空指针 
  5.     if (!mmapID) { 
  6.         return (jlong) kv; 
  7.     } 
  8.     string str = jstring2string(env, mmapID); 
  9.     bool done = false
  10.       // 如果需要进行加密,获取用于加密的 key,最后调用 MMKV::mmkvWithID 
  11.     if (cryptKey) { 
  12.         string crypt = jstring2string(env, cryptKey); 
  13.         if (crypt.length() > 0) { 
  14.             if (relativePath) { 
  15.                 string path = jstring2string(env, relativePath); 
  16.                 kv = MMKV::mmkvWithID(str, DEFAULT_MMAP_SIZE, (MMKVMode) mode, &crypt, &path); 
  17.             } else { 
  18.                 kv = MMKV::mmkvWithID(str, DEFAULT_MMAP_SIZE, (MMKVMode) mode, &crypt, nullptr); 
  19.             } 
  20.             done = true
  21.         } 
  22.     } 
  23.       // 如果不需要加密,则调用 mmkvWithID 不传入加密 key,表示不进行加密 
  24.     if (!done) { 
  25.         if (relativePath) { 
  26.             string path = jstring2string(env, relativePath); 
  27.             kv = MMKV::mmkvWithID(str, DEFAULT_MMAP_SIZE, (MMKVMode) mode, nullptr, &path); 
  28.         } else { 
  29.             kv = MMKV::mmkvWithID(str, DEFAULT_MMAP_SIZE, (MMKVMode) mode, nullptr, nullptr); 
  30.         } 
  31.     } 
  32.     return (jlong) kv; 

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

  1. MMKV *MMKV::mmkvWithID( 
  2.     const std::string &mmapID, int size, MMKVMode mode, string *cryptKey, string *relativePath) { 
  3.     if (mmapID.empty()) { 
  4.         return nullptr; 
  5.     } 
  6.       // 加锁 
  7.     SCOPEDLOCK(g_instanceLock); 
  8.       // 将 mmapID 与 relativePath 结合生成 mmapKey 
  9.     auto mmapKey = mmapedKVKey(mmapID, relativePath); 
  10.       // 通过 mmapKey 在 map 中查找对应的 MMKV 对象并返回 
  11.     auto itr = g_instanceDic->find(mmapKey); 
  12.     if (itr != g_instanceDic->end()) { 
  13.         MMKV *kv = itr->second
  14.         return kv; 
  15.     } 
  16.       // 如果找不到,构建路径后构建 MMKV 对象并加入 map 
  17.     if (relativePath) { 
  18.         auto filePath = mappedKVPathWithID(mmapID, mode, relativePath); 
  19.         if (!isFileExist(filePath)) { 
  20.             if (!createFile(filePath)) { 
  21.                 return nullptr; 
  22.             } 
  23.         } 
  24.         MMKVInfo("prepare to load %s (id %s) from relativePath %s", mmapID.c_str(), mmapKey.c_str(), 
  25.                  relativePath->c_str()); 
  26.     } 
  27.     auto kv = new MMKV(mmapID, size, mode, cryptKey, relativePath); 
  28.     (*g_instanceDic)[mmapKey] = kv; 
  29.     return kv; 

这里的步骤如下:

  • 通过 mmapedKVKey 方法对 mmapID 及 relativePath 进行结合生成了对应的 mmapKey,它会将它们两者的结合经过 md5 从而生成对应的 key,主要目的是为了支持不同相对路径下的同名 mmapID;
  • 通过 mmapKey 在 g_instanceDic 这个 map 中查找对应的 MMKV 对象,如果找到直接返回;
  • 如果找不到对应的 MMKV 对象,构建一个新的 MMKV 对象,加入 map 后返回;
  • 构造 MMKV 对象;

MMKV 的构造函数:

  1. MMKV::MMKV( 
  2.     const std::string &mmapID, int size, MMKVMode mode, string *cryptKey, string *relativePath) 
  3.     : m_mmapID(mmapedKVKey(mmapID, relativePath)) 
  4.     // ...) { 
  5.     // ... 
  6.     if (m_isAshmem) { 
  7.         m_ashmemFile = new MmapedFile(m_mmapID, static_cast<size_t>(size), MMAP_ASHMEM); 
  8.         m_fd = m_ashmemFile->getFd(); 
  9.     } else { 
  10.         m_ashmemFile = nullptr; 
  11.     } 
  12.         // 通过加密 key 构建 AES 加密对象 AESCrypt 
  13.     if (cryptKey && cryptKey->length() > 0) { 
  14.         m_crypter = new AESCrypt((const unsigned char *) cryptKey->data(), cryptKey->length()); 
  15.     } 
  16.         // 赋值操作 
  17.     // 加锁后调用 loadFromFile 加载数据 
  18.     { 
  19.         SCOPEDLOCK(m_sharedProcessLock); 
  20.         loadFromFile(); 
  21.     } 
  • 进行了一些赋值操作,之后如果需要加密则根据用于加密的 cryptKey 生成对应的 AESCrypt 对象用于 AES 加密;
  • 加锁后通过 loadFromFile 方法从文件中读取数据,这里的锁是一个跨进程的文件共享锁;

3、从文件加载数据loadFromFile

我们都知道,MMKV 是基于 mmap 实现的,通过内存映射在高效率的同时保证了数据的同步写入文件,loadFromFile 中就会真正进行内存映射:

  1. void MMKV::loadFromFile() { 
  2.     // ... 
  3.       // 打开对应的文件 
  4.     m_fd = open(m_path.c_str(), O_RDWR | O_CREAT, S_IRWXU); 
  5.     if (m_fd < 0) { 
  6.         MMKVError("fail to open:%s, %s", m_path.c_str(), strerror(errno)); 
  7.     } else { 
  8.           // 获取文件大小 
  9.         m_size = 0; 
  10.         struct stat st = {0}; 
  11.         if (fstat(m_fd, &st) != -1) { 
  12.             m_size = static_cast<size_t>(st.st_size); 
  13.         } 
  14.         // 将文件大小对齐到页大小的整数倍,用 0 填充不足的部分 
  15.         if (m_size < DEFAULT_MMAP_SIZE || (m_size % DEFAULT_MMAP_SIZE != 0)) { 
  16.             size_t oldSize = m_size; 
  17.             m_size = ((m_size / DEFAULT_MMAP_SIZE) + 1) * DEFAULT_MMAP_SIZE; 
  18.             if (ftruncate(m_fd, m_size) != 0) { 
  19.                 MMKVError("fail to truncate [%s] to size %zu, %s", m_mmapID.c_str(), m_size, 
  20.                           strerror(errno)); 
  21.                 m_size = static_cast<size_t>(st.st_size); 
  22.             } 
  23.             zeroFillFile(m_fd, oldSize, m_size - oldSize); 
  24.         } 
  25.           // 通过 mmap 将文件映射到内存 
  26.         m_ptr = (char *) mmap(nullptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0); 
  27.         if (m_ptr == MAP_FAILED) { 
  28.             MMKVError("fail to mmap [%s], %s", m_mmapID.c_str(), strerror(errno)); 
  29.         } else { 
  30.             memcpy(&m_actualSize, m_ptr, Fixed32Size); 
  31.             MMKVInfo("loading [%s] with %zu size in total, file size is %zu", m_mmapID.c_str(), 
  32.                      m_actualSize, m_size); 
  33.             bool loadFromFile = false, needFullWriteback = false
  34.             if (m_actualSize > 0) { 
  35.                 if (m_actualSize < m_size && m_actualSize + Fixed32Size <= m_size) { 
  36.                       // 对文件进行 CRC 校验,如果失败根据策略进行不同对处理 
  37.                     if (checkFileCRCValid()) { 
  38.                         loadFromFile = true
  39.                     } else { 
  40.                           // CRC 校验失败,如果策略是错误时恢复,则继续读取,并且最后需要进行回写 
  41.                         auto strategic = onMMKVCRCCheckFail(m_mmapID); 
  42.                         if (strategic == OnErrorRecover) { 
  43.                             loadFromFile = true
  44.                             needFullWriteback = true
  45.                         } 
  46.                     } 
  47.                 } else { 
  48.                       // 文件大小有误,若策略是错误时恢复,则继续读取,并且最后需要进行回写 
  49.                     auto strategic = onMMKVFileLengthError(m_mmapID); 
  50.                     if (strategic == OnErrorRecover) { 
  51.                         loadFromFile = true
  52.                         needFullWriteback = true
  53.                     } 
  54.                 } 
  55.             } 
  56.               // 从文件中读取内容 
  57.             if (loadFromFile) { 
  58.                 MMKVInfo("loading [%s] with crc %u sequence %u", m_mmapID.c_str(), 
  59.                          m_metaInfo.m_crcDigest, m_metaInfo.m_sequence); 
  60.                   // 读取 MMBuffer 
  61.                 MMBuffer inputBuffer(m_ptr + Fixed32Size, m_actualSize, MMBufferNoCopy); 
  62.                 // 如果需要解密,对文件进行解密 
  63.                   if (m_crypter) { 
  64.                     decryptBuffer(*m_crypter, inputBuffer); 
  65.                 } 
  66.                   // 通过 MiniPBCoder 将 MMBuffer 转换为 Map 
  67.                 m_dic.clear(); 
  68.                 MiniPBCoder::decodeMap(m_dic, inputBuffer); 
  69.                   // 构造用于输出的 CodeOutputData 
  70.                 m_output = new CodedOutputData(m_ptr + Fixed32Size + m_actualSize, 
  71.                                                m_size - Fixed32Size - m_actualSize); 
  72.                 if (needFullWriteback) { 
  73.                     fullWriteback(); 
  74.                 } 
  75.             } else { 
  76.                 SCOPEDLOCK(m_exclusiveProcessLock); 
  77.                 if (m_actualSize > 0) { 
  78.                     writeAcutalSize(0); 
  79.                 } 
  80.                 m_output = new CodedOutputData(m_ptr + Fixed32Size, m_size - Fixed32Size); 
  81.                 recaculateCRCDigest(); 
  82.             } 
  83.             MMKVInfo("loaded [%s] with %zu values", m_mmapID.c_str(), m_dic.size()); 
  84.         } 
  85.     } 
  86.     if (!isFileValid()) { 
  87.         MMKVWarning("[%s] file not valid", m_mmapID.c_str()); 
  88.     } 
  89.     m_needLoadFromFile = false

步骤如下:

  • 打开文件并获取文件大小,将文件的大小对齐到页的整数倍,不足则补 0(与内存映射的原理有关,内存映射是基于页的换入换出机制实现的);
  • 通过 mmap 函数将文件映射到内存中,得到指向该区域的指针 m_ptr;
  • 对文件进行长度校验及 CRC 校验(循环冗余校验,可以校验文件完整性),在失败的情况下会根据当前策略进行抉择,如果策略是失败时恢复,则继续读取,并且在最后将 map 中的内容回写到文件;
  • 通过 m_ptr 构造出一块用于管理 MMKV 映射内存的 MMBuffer 对象,如果需要解密,通过之前构造的 AESCrypt 进行解密;
  • 由于 MMKV 使用了 protobuf 进行序列化,通过 MiniPBCoder::decodeMap 方法将 protobuf 转换成对应的 map;
  • 构造用于输出的 CodedOutputData 类,如果需要回写(CRC 校验或文件长度校验失败),则调用 fullWriteback 方法将 map 中的数据回写到文件;

4、数据写入 

Java 层的 MMKV 对象继承了 SharedPreferences 及 SharedPreferences.Editor 接口并实现了一系列如 putInt、putLong 的方法用于对存储的数据进行修改;

  1. @Override 
  2. public Editor putInt(String keyint value) { 
  3.     encodeInt(nativeHandle, key, value); 
  4.     return this; 

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

  1. extern "C" JNIEXPORT JNICALL jboolean Java_com_tencent_mmkv_MMKV_encodeInt( 
  2.     JNIEnv *env, jobject obj, jlong handle, jstring oKey, jint value) { 
  3.     MMKV *kv = reinterpret_cast<MMKV *>(handle); 
  4.     if (kv && oKey) { 
  5.         string key = jstring2string(env, oKey); 
  6.         return (jboolean) kv->setInt32(value, key); 
  7.     } 
  8.     return (jboolean) false

这里将 Java 层持有的 NativeHandle 转为了对应的 MMKV 对象,之后调用了其 setInt32 方法:

  1. bool MMKV::setInt32(int32_t value, const std::string &key) { 
  2.     if (key.empty()) { 
  3.         return false
  4.     } 
  5.       // 构造值对应的 MMBuffer,通过 CodedOutputData 将其写入 Buffer 
  6.     size_t size = pbInt32Size(value); 
  7.     MMBuffer data(size); 
  8.     CodedOutputData output(data.getPtr(), size); 
  9.     output.writeInt32(value); 
  10.     return setDataForKey(std::move(data), key); 
  • 获取到了写入的 value 在 protobuf 中所占据的大小,之后为其构造了对应的 MMBuffer 并将数据写入了这段 Buffer,最后调用到了 setDataForKey 方法;
  • 同时可以发现 CodedOutputData 是与 Buffer 交互的桥梁,可以通过它实现向 MMBuffer 中写入数据;
  1. bool MMKV::setDataForKey(MMBuffer &&data, const std::string &key) { 
  2.     if (data.length() == 0 || key.empty()) { 
  3.         return false
  4.     } 
  5.       // 获取写锁 
  6.     SCOPEDLOCK(m_lock); 
  7.     SCOPEDLOCK(m_exclusiveProcessLock); 
  8.       // 确保数据已读入内存 
  9.     checkLoadData(); 
  10.     // 将 data 写入 map 中 
  11.     auto itr = m_dic.find(key); 
  12.     if (itr == m_dic.end()) { 
  13.         itr = m_dic.emplace(key, std::move(data)).first
  14.     } else { 
  15.         itr->second = std::move(data); 
  16.     } 
  17.     m_hasFullWriteback = false
  18.     return appendDataWithKey(itr->secondkey); 

数据已读入内存的情况下将 data 写入了对应的 map,之后调用了 appendDataWithKey 方法:

  1. bool MMKV::appendDataWithKey(const MMBuffer &data, const std::string &key) { 
  2.     size_t keyLength = key.length(); 
  3.       // 计算写入到映射空间中的 size 
  4.     size_t size = keyLength + pbRawVarint32Size((int32_t) keyLength); 
  5.     size += data.length() + pbRawVarint32Size((int32_t) data.length()); 
  6.       // 要写入,获取写锁 
  7.     SCOPEDLOCK(m_exclusiveProcessLock); 
  8.       // 确定剩余映射空间足够 
  9.     bool hasEnoughSize = ensureMemorySize(size); 
  10.     if (!hasEnoughSize || !isFileValid()) { 
  11.         return false
  12.     } 
  13.     if (m_actualSize == 0) { 
  14.         auto allData = MiniPBCoder::encodeDataWithObject(m_dic); 
  15.         if (allData.length() > 0) { 
  16.             if (m_crypter) { 
  17.                 m_crypter->reset(); 
  18.                 auto ptr = (unsigned char *) allData.getPtr(); 
  19.                 m_crypter->encrypt(ptr, ptr, allData.length()); 
  20.             } 
  21.             writeAcutalSize(allData.length()); 
  22.             m_output->writeRawData(allData); // note: don't write size of data 
  23.             recaculateCRCDigest(); 
  24.             return true
  25.         } 
  26.         return false
  27.     } else { 
  28.         writeAcutalSize(m_actualSize + size); 
  29.         m_output->writeString(key); 
  30.         m_output->writeData(data); // note: write size of data 
  31.         auto ptr = (uint8_t *) m_ptr + Fixed32Size + m_actualSize - size
  32.         if (m_crypter) { 
  33.             m_crypter->encrypt(ptr, ptr, size); 
  34.         } 
  35.         updateCRCDigest(ptr, size, KeepSequence); 
  36.         return true
  37.     } 
  • 首先计算了即将写入到映射空间的内容大小,之后调用了 ensureMemorySize 方法确保剩余映射空间足够;
  • 如果 m_actualSize 为 0,则会通过 MiniPBCoder::encodeDataWithObject 将整个 map 转换为对应的 MMBuffer,加密后通过 CodedOutputData 写入,最后重新计算 CRC 校验码。否则会将 key 和对应 data 写入,最后更新 CRC 校验码;
  • m_actualSize 是位于文件的首部的,因此是否为 0 取决于文件对应位置;

注意的是:由于 protobuf 不支持增量更新,为了避免全量写入带来的性能问题,MMKV 在文件中的写入并不是通过修改文件对应的位置,而是直接在后面 append 一条新的数据,即使是修改了已存在的 key。而读取时只记录最后一条对应 key 的数据,这样显然会在文件中存在冗余的数据。这样设计的原因我认为是出于性能的考量,MMKV 中存在着一套内存重整机制用于对冗余的 key-value 数据进行处理。它正是在确保内存充足时实现的;

5、内存重整ensureMemorySize

我们接下来看看 ensureMemorySize 是如何确保映射空间是否足够的:

  1. bool MMKV::ensureMemorySize(size_t newSize) { 
  2.     // ... 
  3.     if (newSize >= m_output->spaceLeft()) { 
  4.         // 如果内存剩余大小不足以写入,尝试进行内存重整,将 map 中的数据重新写入 protobuf 文件 
  5.         static const int offset = pbFixed32Size(0); 
  6.         MMBuffer data = MiniPBCoder::encodeDataWithObject(m_dic); 
  7.         size_t lenNeeded = data.length() + offset + newSize; 
  8.         if (m_isAshmem) { 
  9.             if (lenNeeded > m_size) { 
  10.                 MMKVWarning("ashmem %s reach size limit:%zu, consider configure with larger size"
  11.                             m_mmapID.c_str(), m_size); 
  12.                 return false
  13.             } 
  14.         } else { 
  15.             size_t avgItemSize = lenNeeded / std::max<size_t>(1, m_dic.size()); 
  16.             size_t futureUsage = avgItemSize * std::max<size_t>(8, (m_dic.size() + 1) / 2); 
  17.             // 如果内存重整后仍不足以写入,则将大小不断乘2直至足够写入,最后通过 mmap 重新映射文件 
  18.             if (lenNeeded >= m_size || (lenNeeded + futureUsage) >= m_size) { 
  19.                 size_t oldSize = m_size; 
  20.                 do { 
  21.                       // double 空间直至足够 
  22.                     m_size *= 2; 
  23.                 } while (lenNeeded + futureUsage >= m_size); 
  24.                    // ... 
  25.                 if (ftruncate(m_fd, m_size) != 0) { 
  26.                     MMKVError("fail to truncate [%s] to size %zu, %s", m_mmapID.c_str(), m_size, 
  27.                               strerror(errno)); 
  28.                     m_size = oldSize; 
  29.                     return false
  30.                 } 
  31.                   // 用零填充不足部分 
  32.                 if (!zeroFillFile(m_fd, oldSize, m_size - oldSize)) { 
  33.                     MMKVError("fail to zeroFile [%s] to size %zu, %s", m_mmapID.c_str(), m_size, 
  34.                               strerror(errno)); 
  35.                     m_size = oldSize; 
  36.                     return false
  37.                 } 
  38.                                 // unmap 
  39.                 if (munmap(m_ptr, oldSize) != 0) { 
  40.                     MMKVError("fail to munmap [%s], %s", m_mmapID.c_str(), strerror(errno)); 
  41.                 } 
  42.                                 // 重新通过 mmap 映射 
  43.                 m_ptr = (char *) mmap(m_ptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0); 
  44.                 if (m_ptr == MAP_FAILED) { 
  45.                     MMKVError("fail to mmap [%s], %s", m_mmapID.c_str(), strerror(errno)); 
  46.                 } 
  47.                 // check if we fail to make more space 
  48.                 if (!isFileValid()) { 
  49.                     MMKVWarning("[%s] file not valid", m_mmapID.c_str()); 
  50.                     return false
  51.                 } 
  52.             } 
  53.         } 
  54.           // 加密数据 
  55.         if (m_crypter) { 
  56.             m_crypter->reset(); 
  57.             auto ptr = (unsigned char *) data.getPtr(); 
  58.             m_crypter->encrypt(ptr, ptr, data.length()); 
  59.         } 
  60.           // 重新构建并写入数据 
  61.         writeAcutalSize(data.length()); 
  62.         delete m_output; 
  63.         m_output = new CodedOutputData(m_ptr + offset, m_size - offset); 
  64.         m_output->writeRawData(data); 
  65.         recaculateCRCDigest(); 
  66.         m_hasFullWriteback = true
  67.     } 
  68.     return true

内存重整步骤如下:

  • 当剩余映射空间不足以写入需要写入的内容,尝试进行内存重整;
  • 内存重整会将文件清空,将 map 中的数据重新写入文件,从而去除冗余数据;
  • 若内存重整后剩余映射空间仍然不足,不断将映射空间 double 直到足够,并用 mmap 重新映射;

6、删除remove

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

  1. @Override 
  2. public Editor remove(String key) { 
  3.     removeValueForKey(key); 
  4.     return this; 

它调用了 removeValueForKey 这个 Native 方法:

  1. extern "C" JNIEXPORT JNICALL void Java_com_tencent_mmkv_MMKV_removeValueForKey(JNIEnv *env, 
  2.                                                                                jobject instance, 
  3.                                                                                jlong handle, 
  4.                                                                                jstring oKey) { 
  5.     MMKV *kv = reinterpret_cast<MMKV *>(handle); 
  6.     if (kv && oKey) { 
  7.         string key = jstring2string(env, oKey); 
  8.         kv->removeValueForKey(key); 
  9.     } 

这里调用了 Native 层 MMKV 的 removeValueForKey 方法:

  1. void MMKV::removeValueForKey(const std::string &key) { 
  2.     if (key.empty()) { 
  3.         return
  4.     } 
  5.     SCOPEDLOCK(m_lock); 
  6.     SCOPEDLOCK(m_exclusiveProcessLock); 
  7.     checkLoadData(); 
  8.     removeDataForKey(key); 

它在数据读入内存的前提下,调用了 removeDataForKey 方法:

  1. bool MMKV::removeDataForKey(const std::string &key) { 
  2.     if (key.empty()) { 
  3.         return false
  4.     } 
  5.     auto deleteCount = m_dic.erase(key); 
  6.     if (deleteCount > 0) { 
  7.         m_hasFullWriteback = false
  8.         static MMBuffer nan(0); 
  9.         return appendDataWithKey(nan, key); 
  10.     } 
  11.     return false
  • 这里实际上是构造了一条 size 为 0 的 MMBuffer 并调用 appendDataWithKey 将其 append 到 protobuf 文件中,并将 key 对应的内容从 map 中删除;
  • 读取时发现它的 size 为 0,则会认为这条数据已经删除;

7、读取

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

  1. @Override 
  2. public int getInt(String keyint defValue) { 
  3.     return decodeInt(nativeHandle, key, defValue); 

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

  1. extern "C" JNIEXPORT JNICALL jint Java_com_tencent_mmkv_MMKV_decodeInt( 
  2.     JNIEnv *env, jobject obj, jlong handle, jstring oKey, jint defaultValue) { 
  3.     MMKV *kv = reinterpret_cast<MMKV *>(handle); 
  4.     if (kv && oKey) { 
  5.         string key = jstring2string(env, oKey); 
  6.         return (jint) kv->getInt32ForKey(key, defaultValue); 
  7.     } 
  8.     return defaultValue; 

它调用到了 MMKV.getInt32ForKey 方法:

  1. int32_t MMKV::getInt32ForKey(const std::string &key, int32_t defaultValue) { 
  2.     if (key.empty()) { 
  3.         return defaultValue; 
  4.     } 
  5.     SCOPEDLOCK(m_lock); 
  6.     auto &data = getDataForKey(key); 
  7.     if (data.length() > 0) { 
  8.         CodedInputData input(data.getPtr(), data.length()); 
  9.         return input.readInt32(); 
  10.     } 
  11.     return defaultValue; 

调用了 getDataForKey 方法获取到了 key 对应的 MMBuffer,之后通过 CodedInputData 将数据读出并返回;

长度为 0 时会将其视为不存在,返回默认值;

  1. const MMBuffer &MMKV::getDataForKey(const std::string &key) { 
  2.     checkLoadData(); 
  3.     auto itr = m_dic.find(key); 
  4.     if (itr != m_dic.end()) { 
  5.         return itr->second
  6.     } 
  7.     static MMBuffer nan(0); 
  8.     return nan; 

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

MMKV读写是直接读写到mmap文件映射的内存上,绕开了普通读写io需要进入内核,写到磁盘的过程;

总结

MMKV使用的注意事项

1.保证每一个文件存储的数据都比较小,也就说需要把数据根据业务线存储分散。这要就不会把虚拟内存消耗过快;

2.适当的时候释放一部分内存数据,比如在App中监听onTrimMemory方法,在Java内存吃紧的情况下进行MMKV的trim操作;

3.不需要使用的时候,最好把MMKV给close掉,甚至调用exit方法。

【编辑推荐】

  1. MySQL存储过程详解
  2. 通过这三个文件彻底搞懂Rocketmq的存储原理
  3. 如何防止企业的数据和机密从GitHub存储库泄露
  4. 微信又上线新功能,内测深度清理功能,可一键清理微信存储空间
  5. Longhorn 云原生容器分布式存储 - 故障排除指南
【责任编辑:武晓燕 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢

订阅专栏+更多

带你轻松入门 RabbitMQ

带你轻松入门 RabbitMQ

轻松入门RabbitMQ
共4章 | loong576

51人订阅学习

数据湖与数据仓库的分析实践攻略

数据湖与数据仓库的分析实践攻略

助力现代化数据管理:数据湖与数据仓库的分析实践攻略
共3章 | 创世达人

14人订阅学习

云原生架构实践

云原生架构实践

新技术引领移动互联网进入急速赛道
共3章 | KaliArch

42人订阅学习

视频课程+更多

数据库精讲教程

数据库精讲教程

讲师:王艳华6629人学习过

数据结构与算法分析

数据结构与算法分析

讲师:sunnyDLL8248人学习过

PostgreSql入门实操:pg基础入门,安装软件、学习架构、了解基础命令。

PostgreSql入门实操:pg基础入门,安装软件、

讲师:夜傲海338人学习过

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊

51CTO服务号

51CTO官微