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

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

[[429938]]

本文转载自微信公众号「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的使用

使用前请初始化:

MMKV.initialize(this) 
  • 1.

mmkv写入键值对;

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

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

mmkv读取键值对;

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

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

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

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

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

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

二、MMKV 源码解析

1、初始化

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

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

  • 1.
  • 2.
  • 3.
  • 4.

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

public static String initialize(String rootDir) { 
    MMKV.rootDir = rootDir; 
    jniInitialize(MMKV.rootDir); 
    return rootDir; 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

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

extern "C" JNIEXPORT JNICALL void 
Java_com_tencent_mmkv_MMKV_jniInitialize(JNIEnv *env, jobject obj, jstring rootDir) { 
    if (!rootDir) { 
        return
    } 
    const char *kstr = env->GetStringUTFChars(rootDir, nullptr); 
    if (kstr) { 
        MMKV::initializeMMKV(kstr); 
        env->ReleaseStringUTFChars(rootDir, kstr); 
    } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

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

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

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

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

2、获取 MMKV 对象

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

@Nullable 
public static MMKV mmkvWithID(String mmapID, String relativePath) { 
    if (rootDir == null) { 
        throw new IllegalStateException("You should Call MMKV.initialize() first."); 
    } 
    long handle = getMMKVWithID(mmapID, SINGLE_PROCESS_MODE, null, relativePath); 
    if (handle == 0) { 
        return null
    } 
    return new MMKV(handle); 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

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

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

extern "C" JNIEXPORT JNICALL jlong Java_com_tencent_mmkv_MMKV_getMMKVWithID( 
    JNIEnv *env, jobject obj, jstring mmapID, jint mode, jstring cryptKey, jstring relativePath) { 
    MMKV *kv = nullptr; 
      // mmapID 为 null 返回空指针 
    if (!mmapID) { 
        return (jlong) kv; 
    } 
    string str = jstring2string(env, mmapID); 
    bool done = false
      // 如果需要进行加密,获取用于加密的 key,最后调用 MMKV::mmkvWithID 
    if (cryptKey) { 
        string crypt = jstring2string(env, cryptKey); 
        if (crypt.length() > 0) { 
            if (relativePath) { 
                string path = jstring2string(env, relativePath); 
                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 不传入加密 key,表示不进行加密 
    if (!done) { 
        if (relativePath) { 
            string path = jstring2string(env, relativePath); 
            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; 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.

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

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

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.

这里的步骤如下:

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

MMKV 的构造函数:

MMKV::MMKV( 
    const std::string &mmapID, int size, MMKVMode mode, string *cryptKey, string *relativePath) 
    : m_mmapID(mmapedKVKey(mmapID, relativePath)) 
    // ...) { 
    // ... 
    if (m_isAshmem) { 
        m_ashmemFile = new MmapedFile(m_mmapID, static_cast<size_t>(size), MMAP_ASHMEM); 
        m_fd = m_ashmemFile->getFd(); 
    } else { 
        m_ashmemFile = nullptr; 
    } 
        // 通过加密 key 构建 AES 加密对象 AESCrypt 
    if (cryptKey && cryptKey->length() > 0) { 
        m_crypter = new AESCrypt((const unsigned char *) cryptKey->data(), cryptKey->length()); 
    } 
        // 赋值操作 
    // 加锁后调用 loadFromFile 加载数据 
    { 
        SCOPEDLOCK(m_sharedProcessLock); 
        loadFromFile(); 
    } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 进行了一些赋值操作,之后如果需要加密则根据用于加密的 cryptKey 生成对应的 AESCrypt 对象用于 AES 加密;
  • 加锁后通过 loadFromFile 方法从文件中读取数据,这里的锁是一个跨进程的文件共享锁;

3、从文件加载数据loadFromFile

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

void MMKV::loadFromFile() { 
    // ... 
      // 打开对应的文件 
    m_fd = open(m_path.c_str(), O_RDWR | O_CREAT, S_IRWXU); 
    if (m_fd < 0) { 
        MMKVError("fail to open:%s, %s", m_path.c_str(), strerror(errno)); 
    } else { 
          // 获取文件大小 
        m_size = 0; 
        struct stat st = {0}; 
        if (fstat(m_fd, &st) != -1) { 
            m_size = static_cast<size_t>(st.st_size); 
        } 
        // 将文件大小对齐到页大小的整数倍,用 0 填充不足的部分 
        if (m_size < DEFAULT_MMAP_SIZE || (m_size % DEFAULT_MMAP_SIZE != 0)) { 
            size_t oldSize = m_size; 
            m_size = ((m_size / DEFAULT_MMAP_SIZE) + 1) * DEFAULT_MMAP_SIZE; 
            if (ftruncate(m_fd, m_size) != 0) { 
                MMKVError("fail to truncate [%s] to size %zu, %s", m_mmapID.c_str(), m_size, 
                          strerror(errno)); 
                m_size = static_cast<size_t>(st.st_size); 
            } 
            zeroFillFile(m_fd, oldSize, m_size - oldSize); 
        } 
          // 通过 mmap 将文件映射到内存 
        m_ptr = (char *) mmap(nullptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0); 
        if (m_ptr == MAP_FAILED) { 
            MMKVError("fail to mmap [%s], %s", m_mmapID.c_str(), strerror(errno)); 
        } else { 
            memcpy(&m_actualSize, m_ptr, Fixed32Size); 
            MMKVInfo("loading [%s] with %zu size in total, file size is %zu", m_mmapID.c_str(), 
                     m_actualSize, m_size); 
            bool loadFromFile = false, needFullWriteback = false
            if (m_actualSize > 0) { 
                if (m_actualSize < m_size && m_actualSize + Fixed32Size <= m_size) { 
                      // 对文件进行 CRC 校验,如果失败根据策略进行不同对处理 
                    if (checkFileCRCValid()) { 
                        loadFromFile = true
                    } else { 
                          // CRC 校验失败,如果策略是错误时恢复,则继续读取,并且最后需要进行回写 
                        auto strategic = onMMKVCRCCheckFail(m_mmapID); 
                        if (strategic == OnErrorRecover) { 
                            loadFromFile = true
                            needFullWriteback = true
                        } 
                    } 
                } else { 
                      // 文件大小有误,若策略是错误时恢复,则继续读取,并且最后需要进行回写 
                    auto strategic = onMMKVFileLengthError(m_mmapID); 
                    if (strategic == OnErrorRecover) { 
                        loadFromFile = true
                        needFullWriteback = true
                    } 
                } 
            } 
              // 从文件中读取内容 
            if (loadFromFile) { 
                MMKVInfo("loading [%s] with crc %u sequence %u", m_mmapID.c_str(), 
                         m_metaInfo.m_crcDigest, m_metaInfo.m_sequence); 
                  // 读取 MMBuffer 
                MMBuffer inputBuffer(m_ptr + Fixed32Size, m_actualSize, MMBufferNoCopy); 
                // 如果需要解密,对文件进行解密 
                  if (m_crypter) { 
                    decryptBuffer(*m_crypter, inputBuffer); 
                } 
                  // 通过 MiniPBCoder 将 MMBuffer 转换为 Map 
                m_dic.clear(); 
                MiniPBCoder::decodeMap(m_dic, inputBuffer); 
                  // 构造用于输出的 CodeOutputData 
                m_output = new CodedOutputData(m_ptr + Fixed32Size + m_actualSize, 
                                               m_size - Fixed32Size - m_actualSize); 
                if (needFullWriteback) { 
                    fullWriteback(); 
                } 
            } else { 
                SCOPEDLOCK(m_exclusiveProcessLock); 
                if (m_actualSize > 0) { 
                    writeAcutalSize(0); 
                } 
                m_output = new CodedOutputData(m_ptr + Fixed32Size, m_size - Fixed32Size); 
                recaculateCRCDigest(); 
            } 
            MMKVInfo("loaded [%s] with %zu values", m_mmapID.c_str(), m_dic.size()); 
        } 
    } 
    if (!isFileValid()) { 
        MMKVWarning("[%s] file not valid", m_mmapID.c_str()); 
    } 
    m_needLoadFromFile = false

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.

步骤如下: 

  • 打开文件并获取文件大小,将文件的大小对齐到页的整数倍,不足则补 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 的方法用于对存储的数据进行修改;

@Override 
public Editor putInt(String keyint value) { 
    encodeInt(nativeHandle, key, value); 
    return this; 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

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

extern "C" JNIEXPORT JNICALL jboolean Java_com_tencent_mmkv_MMKV_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->setInt32(value, key); 
    } 
    return (jboolean) false

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

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

bool MMKV::setInt32(int32_t value, const std::string &key) { 
    if (key.empty()) { 
        return false
    } 
      // 构造值对应的 MMBuffer,通过 CodedOutputData 将其写入 Buffer 
    size_t size = pbInt32Size(value); 
    MMBuffer data(size); 
    CodedOutputData output(data.getPtr(), size); 
    output.writeInt32(value); 
    return setDataForKey(std::move(data), key); 

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

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.

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

bool MMKV::appendDataWithKey(const MMBuffer &data, const std::string &key) { 
    size_t keyLength = key.length(); 
      // 计算写入到映射空间中的 size 
    size_t size = keyLength + pbRawVarint32Size((int32_t) keyLength); 
    size += data.length() + pbRawVarint32Size((int32_t) data.length()); 
      // 要写入,获取写锁 
    SCOPEDLOCK(m_exclusiveProcessLock); 
      // 确定剩余映射空间足够 
    bool hasEnoughSize = ensureMemorySize(size); 
    if (!hasEnoughSize || !isFileValid()) { 
        return false
    } 
    if (m_actualSize == 0) { 
        auto allData = MiniPBCoder::encodeDataWithObject(m_dic); 
        if (allData.length() > 0) { 
            if (m_crypter) { 
                m_crypter->reset(); 
                auto ptr = (unsigned char *) allData.getPtr(); 
                m_crypter->encrypt(ptr, ptr, allData.length()); 
            } 
            writeAcutalSize(allData.length()); 
            m_output->writeRawData(allData); // note: don't write size of data 
            recaculateCRCDigest(); 
            return true
        } 
        return false
    } else { 
        writeAcutalSize(m_actualSize + size); 
        m_output->writeString(key); 
        m_output->writeData(data); // note: write size of data 
        auto ptr = (uint8_t *) m_ptr + Fixed32Size + m_actualSize - size
        if (m_crypter) { 
            m_crypter->encrypt(ptr, ptr, size); 
        } 
        updateCRCDigest(ptr, size, KeepSequence); 
        return true
    } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 首先计算了即将写入到映射空间的内容大小,之后调用了 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 是如何确保映射空间是否足够的:

bool MMKV::ensureMemorySize(size_t newSize) { 
    // ... 
    if (newSize >= m_output->spaceLeft()) { 
        // 如果内存剩余大小不足以写入,尝试进行内存重整,将 map 中的数据重新写入 protobuf 文件 
        static const int offset = pbFixed32Size(0); 
        MMBuffer data = MiniPBCoder::encodeDataWithObject(m_dic); 
        size_t lenNeeded = data.length() + offset + newSize; 
        if (m_isAshmem) { 
            if (lenNeeded > m_size) { 
                MMKVWarning("ashmem %s reach size limit:%zu, consider configure with larger size"
                            m_mmapID.c_str(), m_size); 
                return false
            } 
        } else { 
            size_t avgItemSize = lenNeeded / std::max<size_t>(1, m_dic.size()); 
            size_t futureUsage = avgItemSize * std::max<size_t>(8, (m_dic.size() + 1) / 2); 
            // 如果内存重整后仍不足以写入,则将大小不断乘2直至足够写入,最后通过 mmap 重新映射文件 
            if (lenNeeded >= m_size || (lenNeeded + futureUsage) >= m_size) { 
                size_t oldSize = m_size; 
                do { 
                      // double 空间直至足够 
                    m_size *= 2; 
                } while (lenNeeded + futureUsage >= m_size); 
                   // ... 
                if (ftruncate(m_fd, m_size) != 0) { 
                    MMKVError("fail to truncate [%s] to size %zu, %s", m_mmapID.c_str(), m_size, 
                              strerror(errno)); 
                    m_size = oldSize; 
                    return false
                } 
                  // 用零填充不足部分 
                if (!zeroFillFile(m_fd, oldSize, m_size - oldSize)) { 
                    MMKVError("fail to zeroFile [%s] to size %zu, %s", m_mmapID.c_str(), m_size, 
                              strerror(errno)); 
                    m_size = oldSize; 
                    return false
                } 
                                // unmap 
                if (munmap(m_ptr, oldSize) != 0) { 
                    MMKVError("fail to munmap [%s], %s", m_mmapID.c_str(), strerror(errno)); 
                } 
                                // 重新通过 mmap 映射 
                m_ptr = (char *) mmap(m_ptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0); 
                if (m_ptr == MAP_FAILED) { 
                    MMKVError("fail to mmap [%s], %s", m_mmapID.c_str(), strerror(errno)); 
                } 
                // check if we fail to make more space 
                if (!isFileValid()) { 
                    MMKVWarning("[%s] file not valid", m_mmapID.c_str()); 
                    return false
                } 
            } 
        } 
          // 加密数据 
        if (m_crypter) { 
            m_crypter->reset(); 
            auto ptr = (unsigned char *) data.getPtr(); 
            m_crypter->encrypt(ptr, ptr, data.length()); 
        } 
          // 重新构建并写入数据 
        writeAcutalSize(data.length()); 
        delete m_output; 
        m_output = new CodedOutputData(m_ptr + offset, m_size - offset); 
        m_output->writeRawData(data); 
        recaculateCRCDigest(); 
        m_hasFullWriteback = true
    } 
    return true

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.

内存重整步骤如下:

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

6、删除remove

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

@Override 
public Editor remove(String key) { 
    removeValueForKey(key); 
    return this; 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

它调用了 removeValueForKey 这个 Native 方法:

extern "C" JNIEXPORT JNICALL void Java_com_tencent_mmkv_MMKV_removeValueForKey(JNIEnv *env, 
                                                                               jobject instance, 
                                                                               jlong handle, 
                                                                               jstring oKey) { 
    MMKV *kv = reinterpret_cast<MMKV *>(handle); 
    if (kv && oKey) { 
        string key = jstring2string(env, oKey); 
        kv->removeValueForKey(key); 
    } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

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

void MMKV::removeValueForKey(const std::string &key) { 
    if (key.empty()) { 
        return
    } 
    SCOPEDLOCK(m_lock); 
    SCOPEDLOCK(m_exclusiveProcessLock); 
    checkLoadData(); 
    removeDataForKey(key); 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

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

bool MMKV::removeDataForKey(const std::string &key) { 
    if (key.empty()) { 
        return false
    } 
    auto deleteCount = m_dic.erase(key); 
    if (deleteCount > 0) { 
        m_hasFullWriteback = false
        static MMBuffer nan(0); 
        return appendDataWithKey(nan, key); 
    } 
    return false

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 这里实际上是构造了一条 size 为 0 的 MMBuffer 并调用 appendDataWithKey 将其 append 到 protobuf 文件中,并将 key 对应的内容从 map 中删除;
  • 读取时发现它的 size 为 0,则会认为这条数据已经删除;

7、读取

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

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

  • 1.
  • 2.
  • 3.
  • 4.

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

extern "C" JNIEXPORT JNICALL jint Java_com_tencent_mmkv_MMKV_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->getInt32ForKey(key, defaultValue); 
    } 
    return defaultValue; 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

它调用到了 MMKV.getInt32ForKey 方法:

int32_t MMKV::getInt32ForKey(const std::string &key, int32_t defaultValue) { 
    if (key.empty()) { 
        return defaultValue; 
    } 
    SCOPEDLOCK(m_lock); 
    auto &data = getDataForKey(key); 
    if (data.length() > 0) { 
        CodedInputData input(data.getPtr(), data.length()); 
        return input.readInt32(); 
    } 
    return defaultValue; 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

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

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

const MMBuffer &MMKV::getDataForKey(const std::string &key) { 
    checkLoadData(); 
    auto itr = m_dic.find(key); 
    if (itr != m_dic.end()) { 
        return itr->second
    } 
    static MMBuffer nan(0); 
    return nan; 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

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

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

总结

MMKV使用的注意事项

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

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

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

 

责任编辑:武晓燕 来源: Android开发编程
相关推荐

2022-02-12 21:05:11

异步爬虫框架

2016-09-22 15:50:38

JavascriptRedux源码解析

2019-09-18 18:12:35

腾讯开源物联网

2021-09-01 06:48:16

AndroidGlide缓存

2021-09-09 06:55:43

AndroidViewDragHel原理

2019-03-10 20:55:11

浏览器Midori开源

2022-06-01 09:28:43

​​Umami​​开源

2021-05-17 09:50:06

Kubebuilde源码CURD

2021-07-03 08:51:30

源码Netty选择器

2021-09-02 07:00:01

Glide流程Android

2013-02-20 14:54:03

C#.NETNDatabase

2022-08-11 08:35:40

Hanko开源

2025-02-19 09:55:39

2019-05-07 14:42:03

深度学习编程人工智能

2009-07-17 14:38:51

轻量级Swing组件

2009-07-14 18:05:28

轻量级Swing组件

2022-05-20 10:32:49

事件循环器事件队列鸿蒙

2015-09-16 09:10:27

Java源码解析

2010-01-06 14:19:47

JSON轻量级

2020-03-31 20:57:50

负载均衡Web服务器开源
点赞
收藏

51CTO技术栈公众号