存储框架-MMKV-mmap高性能 技术的运用

一、简介

MMKV 是基于 mmap 内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。SharedPreferences的替代者,考虑到这个防 crash 方案最主要的诉求还是实时写入,而 mmap 内存映射文件刚好满足这种需求,我们尝试通过它来实现一套 key-value 组件。mmap技术研究,建议先看一下

二、原理

①内存准备:通过mmap内存映射文件,提供一段可供随时写入的内存卡,App只管往里面写数据,有操作系统负责将内存回写到文件,不必担心crash导致数据丢失!
②数据组织:数据序列号选用protobuf协议,pb在性能和空间占用上都有不错的表现。
③写入优化:考虑到使用场景有频繁写入更新,我们需要有增量更新的能力。所以考虑将增量kv对象序列化后,append到内存末尾。
④空间增长:append增量带来一个新的问题,文件大小会增大得不可控。需要再性能和空间上择中;

三、Android使用教程

https://github.com/Tencent/MMKV/blob/master/readme_cn.md

四、性能对比

循环写入随机的int 1k 次,我们有如下性能对比:


image.png

五、源码分析

注意:这里主要分析mmkv怎么使用mmap技术,并进行读写操作的!C++实现方式可以实现多平台(Android、IOS、Windows)公用移植

1、初始化
//java初始化调用
 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");
        }
    //native层初始化
        jniInitialize(rootDir, logLevel2Int(logLevel));
        MMKV.rootDir = rootDir;
        return MMKV.rootDir;
    }
//jni初始化调用
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);
        env->ReleaseStringUTFChars(rootDir, kstr);
    }
}
//真正初始化的地方 master/Core/MMKV.cpp
void MMKV::initializeMMKV(const MMKVPath_t &rootDir, MMKVLogLevel logLevel) {
    g_currentLogLevel = logLevel;

    ThreadLock::ThreadOnce(&once_control, initialize);

    g_rootDir = rootDir;
    mkPath(g_rootDir);

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

启动了一个线程做初始化,然后检查内部路径是否存在,不存在则创建之。

2、获取MMKV对象

获取MMKV对象的方法有以下几个,最傻瓜式的defaultMMKV到最复杂的mmkvWithAshmemID方法,按需调用。
基本都会来到getMMKVWithID方法,然后跳转到MMKV::mmkvWithID
/master/Core/MMKV.cpp

#ifndef MMKV_ANDROID
MMKV *MMKV::mmkvWithID(const string &mmapID, MMKVMode mode, string *cryptKey, MMKVPath_t *rootPath) {

    if (mmapID.empty()) {
        return nullptr;
    }
    SCOPED_LOCK(g_instanceLock);
//查找有没有缓存对象 之前创建的对象使用Map集合缓存起来
    auto mmapKey = mmapedKVKey(mmapID, rootPath);
    auto itr = g_instanceDic->find(mmapKey);
    if (itr != g_instanceDic->end()) {
        MMKV *kv = itr->second;
        return kv;
    }
//判断是否存在文件路径,并加载
    if (rootPath) {
        MMKVPath_t specialPath = (*rootPath) + MMKV_PATH_SLASH + SPECIAL_CHARACTER_DIRECTORY_NAME;
        if (!isFileExist(specialPath)) {
            mkPath(specialPath);
        }
        MMKVInfo("prepare to load %s (id %s) from rootPath %s", mmapID.c_str(), mmapKey.c_str(), rootPath->c_str());
    }
//正在创建nativie对象地方
    auto kv = new MMKV(mmapID, mode, cryptKey, rootPath);
    kv->m_mmapKey = mmapKey;
    (*g_instanceDic)[mmapKey] = kv;
    return kv;
}
#endif

主要工作:创建MMKV对象,包含mmapid、模式、对称秘钥(AES加密)、路径;并使用map集合缓存起来;
MMKV对象构造完毕后,会将该对象的指针地址返回给Java层,Java层的MMKV类会保存住该地址,用于接下来的读写操作。
核心:构造函数中MemoryFile类,里面包含了mmap的操作
构造函数

MMKV::MMKV(const std::string &mmapID, MMKVMode mode, string *cryptKey, MMKVPath_t *rootPath)
    : m_mmapID(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))
    , m_metaFile(new MemoryFile(m_crcPath))
    , m_metaInfo(new MMKVMetaInfo())
    , m_crypter(nullptr)
    , m_lock(new ThreadLock())
    , m_fileLock(new FileLock(m_metaFile->getFd()))
    , m_sharedProcessLock(new InterProcessLock(m_fileLock, SharedLockType))
    , m_exclusiveProcessLock(new InterProcessLock(m_fileLock, ExclusiveLockType))
    , m_isInterProcess((mode & MMKV_MULTI_PROCESS) != 0) {
    m_actualSize = 0;
    m_output = nullptr;

#    ifndef MMKV_DISABLE_CRYPT
    if (cryptKey && cryptKey->length() > 0) {
        m_dicCrypt = new MMKVMapCrypt();
        m_crypter = new AESCrypt(cryptKey->data(), cryptKey->length());
    } else {
        m_dic = new MMKVMap();
    }
#    else
    m_dic = new MMKVMap();
#    endif

    m_needLoadFromFile = true;
    m_hasFullWriteback = false;

    m_crcDigest = 0;

    m_lock->initialize();
    m_sharedProcessLock->m_enable = m_isInterProcess;
    m_exclusiveProcessLock->m_enable = m_isInterProcess;

    // sensitive zone
    {
        SCOPED_LOCK(m_sharedProcessLock);
        loadFromFile();
    }
}
#endif

MemoryFile分析

bool MemoryFile::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_name.c_str(), strerror(errno));
        m_ptr = nullptr;
        return false;
    }

    return true;
}

MMKV 的核心,它调用了 mmap 函数来将该文件映射到了 m_ptr 指向的内存为起点的内存中。整个项目就围绕m_ptr对文件与映射内存进行操作!

3、从文件读取数据
bool MMKV::getString(MMKVKey_t key, string &result) {
    if (isKeyEmpty(key)) {
        return false;
    }
    SCOPED_LOCK(m_lock);
    auto data = getDataForKey(key);
    if (data.length() > 0) {
        try {
            CodedInputData input(data.getPtr(), data.length());
            result = input.readString();
            return true;
        } catch (std::exception &exception) {
            MMKVError("%s", exception.what());
        }
    }
    return false;
}

通过getDataForKey()函数获取数据MMBuffer对象,里面包含数据指针和数据长度;

4、写数据
//java层调用
 public boolean encode(String key, String value) {
        return this.encodeString(this.nativeHandle, key, value);
    }
//jni调用
MMKV_JNI jboolean encodeString(JNIEnv *env, jobject, jlong handle, jstring oKey, jstring oValue) {
    MMKV *kv = reinterpret_cast(handle);
    if (kv && oKey) {
        string key = jstring2string(env, oKey);
        if (oValue) {
            string value = jstring2string(env, oValue);
            return (jboolean) kv->set(value, key);
        } else {
            kv->removeValueForKey(key);
            return (jboolean) true;
        }
    }
    return (jboolean) false;
}
//native调用
bool MMKV::set(const string &value, MMKVKey_t key) {
    if (isKeyEmpty(key)) {
        return false;
    }
    return setDataForKey(MMBuffer((void *) value.data(), value.length(), MMBufferNoCopy), key, true);
}

最底层调用master/Core/MMKV_IO.cpp

bool MMKV::setDataForKey(MMBuffer &&data, MMKVKey_t key, bool isDataHolder) {
    if ((!isDataHolder && data.length() == 0) || isKeyEmpty(key)) {
        return false;
    }
    SCOPED_LOCK(m_lock);
    SCOPED_LOCK(m_exclusiveProcessLock);
    checkLoadData();
......
auto ret = appendDataWithKey(data, key, isDataHolder);
......
}
//最后调用doAppendDataWithKey方法执行mmap写入文件操作
KVHolderRet_t
MMKV::doAppendDataWithKey(const MMBuffer &data, const MMBuffer &keyData, bool isDataHolder, uint32_t originKeyLength) {
 auto isKeyEncoded = (originKeyLength < keyData.length());
    auto keyLength = static_cast(keyData.length());
    auto valueLength = static_cast(data.length());
........
//这里需要对key进行编码后,重新计算长度;
 // size needed to encode the key
    size_t size = isKeyEncoded ? keyLength : (keyLength + pbRawVarint32Size(keyLength));
    // size needed to encode the value
    size += valueLength + pbRawVarint32Size(valueLength);

    SCOPED_LOCK(m_exclusiveProcessLock);

    bool hasEnoughSize = ensureMemorySize(size);
    if (!hasEnoughSize || !isFileValid()) {
        return make_pair(false, KeyValueHolder());
    }
//--核心:真正写入操作的地方
.....
 try {
//写入key
        if (isKeyEncoded) {
            m_output->writeRawData(keyData);
        } else {
            m_output->writeData(keyData);
        }
        if (isDataHolder) {
            m_output->writeRawVarint32((int32_t) valueLength);
        }
//写入data
        m_output->writeData(data); // note: write size of data
    } catch (std::exception &e) {
        MMKVError("%s", e.what());
        return make_pair(false, KeyValueHolder());
    }

    auto offset = static_cast(m_actualSize);
    auto ptr = (uint8_t *) m_file->getMemory() + Fixed32Size + m_actualSize;
.......

}

在写入数据前,先计算key+data数据长度size,再计算可用空间是否满足写入,不满足或文件无效则返回失败;
核心:CodedOutputData先数据先写入编码后的key,再连续写入data数据;

CodedOutputData操作分析
void CodedOutputData::writeData(const MMBuffer &value) {
    this->writeRawVarint32((int32_t) value.length());
    this->writeRawData(value);
}

void CodedOutputData::writeRawData(const MMBuffer &data) {
    size_t numberOfBytes = data.length();
    if (m_position + numberOfBytes > m_size) {
        auto msg = "m_position: " + to_string(m_position) + ", numberOfBytes: " + to_string(numberOfBytes) +
                   ", m_size: " + to_string(m_size);
        throw out_of_range(msg);
    }

    memcpy(m_ptr + m_position, data.getPtr(), numberOfBytes);
//记录写入位置
    m_position += numberOfBytes;
}

memcpy()是C 库函数 从存储区 data 复制 numberOfBytes 个字节到存储区m_ptr + m_position
核心思想:上面代码可以看出,实际上就是通过memcpy的方式将数据复制到m_ptr所指向的那块内存中,由于这块内存是与文件形成映射的,所以文件的内容也会被系统自动回写

六、总结

MMKV是基于mmap的K-V存储库,完全可以替代SharedPreferences使用,支持加解密功能以及多进程操作;而且效率高于SharedPreferences近百倍速度,原因在于使用了mmap这种内存映射技术的实现,节省了拷贝和提交时间;
当写入空间不足时,会进行文件重整。将所有数据重新序列化一次,若文件重整后内存仍然不足,则会将文件进行double扩容。最后,会将文件清空重新写入重整后的数据;

你可能感兴趣的:(存储框架-MMKV-mmap高性能 技术的运用)