android KV存储三部曲之MMKV

670 阅读5分钟

Android MMKV

应该说时Tencent MMKV

github地址:github.com/Tencent/MMK…

看下介绍:

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

MMKV 是基于 mmap 内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。从 2015 年中至今在微信上使用,其性能和稳定性经过了时间的验证。近期也已移植到 Android / macOS / Win32 / POSIX 平台,一并开源。

官方介绍的原理

  • 内存准备
    通过 mmap 内存映射文件,提供一段可供随时写入的内存块,App 只管往里面写数据,由操作系统负责将内存回写到文件,不必担心 crash 导致数据丢失。
  • 数据组织
    数据序列化方面我们选用 protobuf 协议,pb 在性能和空间占用上都有不错的表现。
  • 写入优化
    考虑到主要使用场景是频繁地进行写入更新,我们需要有增量更新的能力。我们考虑将增量 kv 对象序列化后,append 到内存末尾。
  • 空间增长
    使用 append 实现增量更新带来了一个新的问题,就是不断 append 的话,文件大小会增长得不可控。我们需要在性能和空间上做个折中。

使用

很简单,直接看github官网地址就可以。 在application中初始化,activity中使用。

//1.初始化
MMKV rootDir = MMKV.initialize(this)

//2.获取实例
MMKV kv = MMKV.defaultMMKV();
//取值
boolean bValue = kv.decodeBool("bool");
//3.存值
kv.encode("int", Integer.MIN_VALUE);
//4.取值
int iValue = kv.decodeInt("int");

引一张官网的对比图(循环写入随机的int 1k次):

image.png 还搞什么SP优化?

要看MMKV的流程,肯定要把源码下载,这里推荐我的小伙伴使用sublime Text来查看源码,非常方便,截个图感受一下

截屏2022-11-17 00.07.38.png

1.初始化

     //初始化方法 这里只看关键代码
    public static String initialize(Context context) {
        //向下调用
        initialize(context, root, null, logLevel);
        //initialize调用doInitialize
        doInitialize(rootDir, cacheDir, loader, logLevel);
        //doInitialize调用jniInitialize
        //关键代码,看名字就知道是一个jni调用c++代码
        jniInitialize(rootDir,cacheDir,logLevel2Int(logLevel));
        
   MMKV_JNI void jniInitialize(JNIEnv *env, jobject obj, jstring rootDir, jstring cacheDir, jint logLevel) {
    if (!rootDir) {
        return;
    }
    const char *kstr = env->GetStringUTFChars(rootDir, nullptr);
    if (kstr) {
        //调用MMKV.cpp方法
        MMKV::initializeMMKV(kstr, (MMKVLogLevel) logLevel);
        env->ReleaseStringUTFChars(rootDir, kstr);

        g_android_tmpDir = jstring2string(env, cacheDir);
    }
}
//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());
}

这里我们看,实际上初始化就是做了创建存储的根目录,记录rootDir,没别的事情了。

2.获取实例

//获取实例
MMKV kv = MMKV.defaultMMKV();
/**调用链
*MMKV.class          defaultMMKV()
*navtive-bridge.cpp  getDefaultMMKV
*MMKV.CPP.           mmkvWithID
**/
#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);
    
    //参数1 mmapID就是keyname 参数2 存储地址
    //1.经过md5,生成了一个mmapkey
    auto mmapKey = mmapedKVKey(mmapID, rootPath);
    //2.通过mmapkey查找是否有对象,有就返回
    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());
    }
    //3.没有找到就创建一个新的MMKV对象加入到map
    auto kv = new MMKV(mmapID, mode, cryptKey, rootPath);
    kv->m_mmapKey = mmapKey;
    (*g_instanceDic)[mmapKey] = kv;
    re
  • 1.mmapedKVKey方法通过两个参数mmapID、rootPath,经过med生成mmapkey
  • 2.通过mmapkey在g_instanceDic中进行查找是否有相应对象
  • 3.没有找到对象,创建一个新的MMKV添加到map中

3.取值

以前面的代码为例

//通过decodeBool取对应的值,调用到了jni的方法
MMKV_JNI jboolean decodeBool(JNIEnv *env, jobject, jlong handle, jstring oKey, jboolean defaultValue) {
    //将okey转换成mmkv对象
    MMKV *kv = reinterpret_cast<MMKV *>(handle);
    if (kv && oKey) {
        string key = jstring2string(env, oKey);
        return (jboolean) kv->getBool(key, defaultValue);
    }
    return defaultValue;
}

bool MMKV::getBool(MMKVKey_t key, bool defaultValue, bool *hasValue) {
    if (isKeyEmpty(key)) {
        if (hasValue != nullptr) {
            *hasValue = false;
        }
        return defaultValue;
    }
    SCOPED_LOCK(m_lock);
    SCOPED_LOCK(m_sharedProcessLock);
    //通过key个人理解时拿到了具柄之类的数据
    auto data = getDataForKey(key);
    if (data.length() > 0) {
        try {
            //通过具柄获取到真正的数据,通过input.readBool返回
            CodedInputData input(data.getPtr(), data.length());
            if (hasValue != nullptr) {
                *hasValue = true;
            }
            return input.readBool();
        } catch (std::exception &exception) {
            MMKVError("%s", exception.what());
        }
    }
    if (hasValue != nullptr) {
        *hasValue = false;
    }
    return defaultValue;
}

  • 1.取值方法通过jni调用到了c++层
  • 2.通过mmkv:getBool方法寻找对应的值
  • 3.通过getDataForkey拿到数据具柄
  • 4.通过CodeInputData方法传入具柄和长度获取到存值
  • 5.通过input.readBool将值返回回来

4.存值

//依旧以前面的代码为例
//存值
kv.encode("int", Integer.MIN_VALUE);

//这里有很多重载函数 根据你传入的类型调用到具体的方法
//找一个string方法的
   public boolean encode(String key, @Nullable String value) {
        return encodeString(nativeHandle, key, value);
    }

//调入natvie-bridge.cpp中
MMKV_JNI jboolean encodeString(JNIEnv *env, jobject, jlong handle, jstring oKey, jstring oValue) {
    //此处的handler就是通过MMKVWithID获取的一个long类型 对象
    //相当于一个mmkv地址,将mmkv地址转换成对象
    MMKV *kv = reinterpret_cast<MMKV *>(handle);
    if (kv && oKey) {
        //将jstring转换成string
        string key = jstring2string(env, oKey);
        if (oValue) {
            string value = jstring2string(env, oValue);
            //将值通过key set进去
            return (jboolean) kv->set(value, key);
        } else {
            kv->removeValueForKey(key);
            return (jboolean) true;
        }
    }
    return (jboolean) false;
}

bool MMKV::set(bool value, MMKVKey_t key) {
    //如果key是空的返回 false
    if (isKeyEmpty(key)) {
        return false;
    }
    size_t size = pbBoolSize();
    MMBuffer data(size);
    //通过具柄拿到数据
    CodedOutputData output(data.getPtr(), size);
    //将数据写入
    output.writeBool(value);
    //调用到MMKV_IO.cpp中进行io读写,下面具体说
    return setDataForKey(move(data), key);
}

存值的方法和取值类似

  • 1.通过方法调用到jni方法中,调用到jni层到C++ encodeString
  • 2.通过具柄转换成的mmkv对象
  • 3.通过CodeOutputData传入数据具柄找到对应数据
  • 4.写入数据,进行io操作

5.mmap内存映射

//构造mmvk对象
auto kv = new MMKV(mmapID, mode, cryptKey, rootPath);

MMKV::MMKV(const string &mmapID, int size, MMKVMode mode, string *cryptKey, string *rootPath){

    // force use fcntl(), otherwise will conflict with MemoryFile::reloadFromFile()
    //通过文件路径构建锁之类的操作
    m_fileModeLock = new FileLock(m_file->getFd(), true);
    m_sharedProcessModeLock = new InterProcessLock(m_fileModeLock, SharedLockType);
    m_exclusiveProcessModeLock = nullptr;

#    ifndef MMKV_DISABLE_CRYPT
    //加密算法 加密key,new 一个加密对象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
    {
        //加锁
        SCOPED_LOCK(m_sharedProcessLock);
        //文件数据加载
        loadFromFile();
    }
}

//理论上应该在loadFromFile()中进行mmap。但是并没有找到相关代码
//埋个坑 等后续有时间阅读下官方文档具体看下