MMKV基本使用与源码解析

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();
    @Override
    public 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)); // ... 1
        return 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); // ... 1
        env->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); // ... 1

    MMKVInfo("root dir: " MMKV_PATH_FORMAT, g_rootDir.c_str());
}

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

     2.2 获取MMKV对象

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

    @Nullable
    public 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::mmkvWithID
        string 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 结合生成 mmapKey
    auto 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 加密对象 AESCrypt
    if (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_CRYPT
    if (m_crypter) {
        if (m_metaInfo->m_version >= MMKVVersionRandomIV) {
            m_crypter->resetIV(m_metaInfo->m_vector, sizeof(m_metaInfo->m_vector));
        }
    }
#endif
    if (!m_file->isFileValid()) {
        m_file->reloadFromFile(); // ... 1
    }
    if (!m_file->isFileValid()) {
        MMKVError("file [%s] not valid", m_path.c_str());
    } else {
        // error checking
        bool 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_CRYPT
                if (m_crypter) {
                    MiniPBCoder::greedyDecodeMap(*m_dicCrypt, inputBuffer, m_crypter);
                } else
#endif
                {
                    MiniPBCoder::greedyDecodeMap(*m_dic, inputBuffer);
                }
            } else {
#ifndef MMKV_DISABLE_CRYPT
                if (m_crypter) {
                    MiniPBCoder::decodeMap(*m_dicCrypt, inputBuffer, m_crypter);
                } else
#endif
                {
                    MiniPBCoder::decodeMap(*m_dic, inputBuffer);
                }
            }
            // 构造用于输出的 CodeOutputData
            m_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 everything
            SCOPED_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_ANDROID
    if (m_fileType == MMFILE_TYPE_ASHMEM) {
        return;
    }
#    endif
    if (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_IOS
        tryResetFileProtection(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 为例:

    @Override
    public 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 将其写入 Buffer
    MMBuffer 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 方法可以实现删除操作:    

    @Override
    public 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_CRYPT
    if (m_crypter) {
        auto itr = m_dicCrypt->find(key);
        if (itr != m_dicCrypt->end()) {
            m_hasFullWriteback = false;
            static MMBuffer nan;
#    ifdef MMKV_APPLE
            auto ret = appendDataWithKey(nan, key, itr->second);
            if (ret.first) {
                auto oldKey = itr->first;
                m_dicCrypt->erase(itr);
                [oldKey release];
            }
#    else
            auto ret = appendDataWithKey(nan, key);
            if (ret.first) {
                m_dicCrypt->erase(itr);
            }
#    endif
            return 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_APPLE
                auto oldKey = itr->first;
                m_dic->erase(itr);
                [oldKey release];
#else
                m_dic->erase(itr);
#endif
            }
            return ret.first;
        }
    }

    return false;
}

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

     2.5 读取数据

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

    @Override
    public 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_CRYPT
    if (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 从而进行内存重整与扩容。

猜你喜欢

转载自blog.csdn.net/lixiong0713/article/details/107998965