MMKV与mmap:全方位解析

news2024/9/22 5:38:16

概述

MMKV 是基于 mmap 内存映射的移动端通用 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。从 2015 年中至今,在 iOS 微信上使用已有近 3 年,其性能和稳定性经过了时间的验证。近期已移植到 Android 平台。在腾讯内部开源半年之后,得到公司内部团队的广泛应用和一致好评。

MMKV 原理

内存准备:

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

数据组织:

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

写入优化:

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

空间增长:

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

mmap原理

内存映射:

mmap实现了一种使用内存映射到磁盘文件的方法,将本该属于磁盘文件的对象 映射到进程地址空间中,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动(默认并不实时)回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数,对文件直接通过内存映射读取从而跨过了页缓存,减少数据拷贝次数,用内存读写取代I/O读写,提高文件读取效率。

另外,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享,从而达到进程间通信和进程间共享的目的。简言之,很强大。

mmap 内存映射(memory mapping)

下面大致了解下mmap内存映射原理:

mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。如下图所示:

1、对文件的读取操作跨过了页缓存,减少了数据的拷贝次数,用内存读写取代I/O读写,提高了文件读取效率。

2、实现了用户空间和内核空间的高效交互方式。两空间的各自修改操作可以直接反映在映射的区域内,从而被对方空间及时捕捉。

3、提供进程间共享内存及相互通信的方式。不管是父子进程还是无亲缘关系的进程,都可以将自身用户空间映射到同一个文件或匿名映射到同一片区域。从而通过各自对映射区域的改动,达到进程间通信和进程间共享的目的。 同时,如果进程A和进程B都映射了区域C,当A第一次读取C时通过缺页从磁盘复制文件页到内存中;但当B再读C的相同页面时,虽然也会产生缺页异常,但是不再需要从磁盘中复制文件过来,而可直接使用已经保存在内存中的文件数据。

4、可用于实现高效的大规模数据传输。内存空间不足,是制约大数据操作的一个方面,解决方案往往是借助硬盘空间协助操作,补充内存的不足。但是进一步会造成大量的文件I/O操作,极大影响效率。这个问题可以通过mmap映射很好的解决。换句话说,但凡是需要用磁盘空间代替内存的时候,mmap都可以发挥其功效。

Protobuf协议

protobuf(Google Protocol Buffers)是Google提供一个具有高效的协议数据交换格式工具库(类似Json),但相比于Json,Protobuf有更高的转化效率,时间效率和空间效率都是JSON的3-5倍。  数据表示方式:每块数据由接连的若干个字节表示(小的数据用1个字节就可以表示),每个字节最高位标识本块数据是否结束(1:未结束,0:结束),低7位表示数据内容。(可以看出数据封包后体积至少增大14.2%)

例子:

数字1的表示方法为:0000 0001,这个容易理解 数字300的表示方法为:1010 1100 0000 0010 因为1表示未结束,须将标识位置移去,所以这个数字实际是0000 0010 1010 1100

1010 1100 0000 0010
→ 010 1100  000 0010//移去标识位
如下:
000 0010  010 1100
→  000 0010 ++ 010 1100 //拼接
→  10 0101100
→  256 + 32 + 8 + 4 = 300//计算

实际使用的时候,protobuf最后其实会转化成一长串的二进制,二进制的形式其实就可在任何平台传输了,这里有个问题就是怎么一大串的二进制怎么隔开数据呢? 做法就是每块数据前加一个数据头,表示数据类型及协议字段序号。 msg1_head + msg1 + msg2_head + msg2 + … 数据头也是基于128bits的数值存储方式,一般1个字节就可以表示:

message Person {
    required int32 name = 1;//required 表示必须填入
}
如上创建了 Person 的结构并且把 name 设为 2,序列化好的二进制数据为:
0000 1000 0000 0010//以上数据转成十六进制也就是 08 02
//000 1000
//低3位表示数据类型:0,其他表示协议字段序号:1,加上最高位0, 结果就是8

简而言之,protobuf有着可跨平台的传输能力,快速转化的效率

1、序列化和反序列化效率比 xml 和 json 都高

2、字段可以乱序,欠缺,因此可以用来兼容旧的协议,或者是减少协议数据。

简单使用

相对于SP来说,mmkv的使用更为简单,只不过这里的初识化流程需要我们手动添加到Application中(保证使用前调用即可)。 下为Android java调用实例:

//初识化
MMKV.initialize(this); //这种是默认初识化,会创建默认的存储路径和日志等级打印
MMKV.initialize(this,"rootDir", MMKVLogLevel.LevelError);//当然,也可以修改存储路径和日志等级
//获取MMKV对象
MMKV mmkv=MMKV.defaultMMKV();//默认的mapid为mmkv.default
MMKV mmkv1=MMKV.mmkvWithID("1234");//也可修改mapid 类似getSharedPreferences("1234", Context.MODE_PRIVATE); 里的表名
//对象方法
mmkv.putInt("123",123);//存储数据 
mmkv.getInt("123",1235);//获取数据

Android调用可直接依赖

dependencies {
    implementation 'com.tencent:mmkv-static:1.2.10'
    // replace "1.2.10" with any available version
}

深入源码

因为MMKV的核心代码是由C语言编译的,对于Android端引来的Jar更多的是进行JNI的调用,所以在下面代码分析的时候更多的偏向于C的调用逻辑,至于Jar包中的调用流程不再放入。 大致剖析流程如下:

初识化 MMKV.initialize(this)

MMKV的初始化主要目的其实是对于mmkv的数据存储路径是否已经创建了,内部代码对多次初识化和多线程同时初识化进行了线程保护,这点可以学习。

void initialize() {
    g_instanceDic = new unordered_map<string, MMKV *>;//获取一个 unordered_map
    g_instanceLock = new ThreadLock();
    g_instanceLock->initialize();
    //使用getpagesize函数获得一页内存大小
    //系统给我们提供真正的内存时,用页为单位提供,一次最少提供一页的真实内存空间
    //分配内存空间:你真实的分配了多少内存,就使用多少内存,不要越界使用
    //但是系统提供的真实内存空间是以页来提供的。
    mmkv::DEFAULT_MMAP_SIZE = mmkv::getPageSize();
    MMKVInfo("version %s, page size %d, arch %s", MMKV_VERSION, DEFAULT_MMAP_SIZE, MMKV_ABI);
}
​
void MMKV::initializeMMKV(const MMKVPath_t &rootDir, MMKVLogLevel logLevel) {
    g_currentLogLevel = logLevel;
    ThreadLock::ThreadOnce(&once_control, initialize);
    // 引入了ThreadLock库 最后实际 由pthread_once()指定的函数执行且仅执行一次,而once_control则表征是否执行过。
    //简单来说就是 pthread_once() 方法只保证这个方法只走了一次 如果多个线程同时调用,最先进入的会通过互斥锁让其他线程等待直到释放 如何其他监测到已经执行完成也会停止执行
    g_rootDir = rootDir;
    mkPath(g_rootDir);//MemoryFile.cpp  创建路径
    MMKVInfo("root dir: " MMKV_PATH_FORMAT, g_rootDir.c_str());//输出日志信息
}
​
//MemoryFile.cpp  创建路径
extern bool mkPath(const MMKVPath_t &str) {
    char *path = strdup(str.c_str());
    struct stat sb = {};
    bool done = false;
    char *slash = path;
    while (!done) {
        slash += strspn(slash, "/");
        slash += strcspn(slash, "/");
        done = (*slash == '\0');
        *slash = '\0';
        if (stat(path, &sb) != 0) {
            if (errno != ENOENT || mkdir(path, 0777) != 0) {
                MMKVWarning("%s : %s", path, strerror(errno));
                free(path);
                return false;
            }
        } else if (!S_ISDIR(sb.st_mode)) {
            MMKVWarning("%s: %s", path, strerror(ENOTDIR));
            free(path);
            return false;
        }
​
        *slash = '/';
    }
    free(path);
    return true;
}
​
void ThreadLock::ThreadOnce(ThreadOnceToken_t *onceToken, void (*callback)()) {
    pthread_once(onceToken, callback);
    //pthread_once()都必须等待其中一个激发”已执行一次”信号,因此所有pthread_once()都会陷入永久的等待中;如果设为2,则表示该函数已执行过一次,从而所有pthread_once()都会立即返回0。
}

获取mmkv对象 MMKV mmkv=MMKV.defaultMMKV();

在获取mmkv对象的时候会先遍历一个g_instanceDic 无序map表,看看内部是否已经存在和这个mapID相关联的mmkv对象,如果已经存储了就直接取出使用,如果未存储则重新创建一个MMKV对象,同时加上了区域锁(SCOPED_LOCK(g_instanceLock)),可以规定哪部分可以被该线程访问,结束会自动释放 解决了同一文件不会产生线程冲突还能被同时多线程访问.

MMKV *MMKV::defaultMMKV(MMKVMode mode, string *cryptKey) {
#ifndef MMKV_ANDROID //预定的宏编译
    return mmkvWithID(DEFAULT_MMAP_ID, mode, cryptKey);//移动端走该方法   DEFAULT_MMAP_ID   "mmkv.default"  MMKVPredef.h
#else
    return mmkvWithID(DEFAULT_MMAP_ID, DEFAULT_MMAP_SIZE, mode, cryptKey);
#endif
}
​
unordered_map<std::string, MMKV *> *g_instanceDic;
//unordered_map内部实现了一个哈希表(也叫散列表,通过把关键码值映射到Hash表中一个位置来访问记录,查找的时间复杂度可达到O(1),其在海量数据处理中有着广泛应用)。因此,其元素的排列顺序是无序的。
//哈希表的建立比较耗费时间
//适用处,对于查找问题,unordered_map会更加高效一些,因此遇到查找问题,常会考虑一下用unordered_map
​
MMKV *MMKV::mmkvWithID(const string &mmapID, MMKVMode mode, string *cryptKey, MMKVPath_t *rootPath) { //这里第四个参数其实是加密值 实际也是存储路径
    if (mmapID.empty()) {
        return nullptr;
    }//mmapID不能为空
    SCOPED_LOCK(g_instanceLock); //g_instanceLock = new ThreadLock();  区域锁  可以规定哪部分可以被该线程访问,结束会自动释放  解决了同一文件不会产生线程冲突还能被同时多线程访问
​
    auto mmapKey = mmapedKVKey(mmapID, rootPath);
    auto itr = g_instanceDic->find(mmapKey); //通过给定主键查找元素,没找到:返回unordered_map::end
    if (itr != g_instanceDic->end()) {//查找itr是否在map中  ?这里的写法可能有点多余 上面已经查找过了,这里又查一遍
        MMKV *kv = itr->second;// the mapped value (of type T)
        return kv;
    }//这个mapID其实是存在哈希表内的,如果要创建多个线程都要操作这个map表,那么这时候就需要通过mmap决定是否存在其他mapID的mmkv对象 保证同一mmapkey绑定的对象只有一个
    
    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());
    }//加密值不为空
    
    auto kv = new MMKV(mmapID, mode, cryptKey, rootPath);
    kv->m_mmapKey = mmapKey;
    (*g_instanceDic)[mmapKey] = kv;//将创建的kv对象放入表内
    return kv;
}
//返回根据mmapID的加密值 这个mapID其实是绑定线程操作表的,如果要创建多个线程都要操作这个map表,那么这时候就需要通过mmap决定是否存在其他mapID线程 保证同一mmapkey绑定的线程只有一个在运行
string mmapedKVKey(const string &mmapID, MMKVPath_t *rootPath) {
    if (rootPath && g_rootDir != (*rootPath)) {
        return md5(*rootPath + MMKV_PATH_SLASH + string2MMKVPath_t(mmapID));//MMKV_PATH_SLASH  默认分割符  返回根据mmapID创建的地址
    }
    return mmapID;
}
​

创建MMKV对象,通过mmapID获取文件存放目录,获取文件存储目录用于件载入,这里将载入的文件作为memoryFile对象,初识化各类线程锁,这里还有个crc文件是对数据进行校验的,区别有效数据和无效数据,具体原理这里不做展开。

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))/// 拼装 .crc 文件路径 考虑到文件系统、操作系统都有一定的不稳定性,增加了 crc 校验,对无效数据进行甄别。
    , 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
     根据 cryptKey 创建 AES 加解密的引擎
    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;//是否需要加载文件 对于未使用的mmapId首次都是需要从文件加载数据到内存
    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();//核心方法
    }
}
​

载入文件到缓存中,通过判断文件是否有效拿到对应的文件对象,将数据构建输入到换内存页中,这里有个dic对照表,将缓存数据放入,后续保持和dic的映射同步即可,因为后续的写入会由文件系统自动写入,即使程序出现crash,正在写入的线程也不会被影响

上图为该方法大致载入流程

MMKV维护了一个<String,AnyObject>的dic,在写入数据时,会在dit和mmap映射区写入相同的数据,最后由内核同步到文件。因为dic和文件数据同步,所以读取时直接去dit中的值。MMKV数据持久化的步骤:mmap 内存映射 -> 写数据 -> 读数据 -> crc校验 -> aes加密。

其中因为文件不同于内存中的对象,文件是持久存在的,而内存中的实例对象是会被回收的。 当我创建一个实例对象的时候,先要检查是否已经存在以往的映射文件, 若存在,需要先建立映射 关系,然后解析出以往的数据;若不存在,才是直接创建空文件来建立映射关系。

void MMKV::loadFromFile() {
    if (m_metaFile->isFileValid()) {
        m_metaInfo->read(m_metaFile->getMemory());//m_metaFile 文件的映射
    }
#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();//如果文件不是有效的(文件大小等待),则重新进行加载   m_file:m_file(new MemoryFile(m_path))这里获取的对象
    }
    if (!m_file->isFileValid()) {
        MMKVError("file [%s] not valid", m_path.c_str());//重新加载后仍然无效则报错
    } else {
        // error checking
        bool loadFromFile = false, needFullWriteback = false;
        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 inputBuffer(ptr + Fixed32Size, m_actualSize, MMBufferNoCopy);
            //先清空 后写入
            if (m_crypter) {
                clearDictionary(m_dicCrypt);
            } else {
                clearDictionary(m_dic);
            }
            // 进行写入 Protobuf
            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);
                }
            }
            // 构建输出数据
            m_output = new CodedOutputData(ptr + Fixed32Size, m_file->getFileSize() - Fixed32Size);
            m_output->seek(m_actualSize);
              // 进行重整回写, 剔除重复的数据
            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;
}
​

数据写入 mmkv.put

put方法实际执行的是encodeInt方法,也就是MMKV.cpp里面的set方法,在申请映射内存时是按页来计算的,默认一页是1024字节,每次将数据写入前会先判断映射内存是否有足够的空间进行写入,如果空间不够就会进行扩容,每次扩容都是原理扩容的两倍,也就是前面提到的空间增长。动态的申请内存空间,用官方的话来说就是在性能和空间上做个折中。

// 写入32位整型
bool MMKV::set(int32_t value, MMKVKey_t key) {
    if (isKeyEmpty(key)) {
        return false;
    }//传入key值不可为空字符串
    size_t size = pbInt32Size(value);
    MMBuffer data(size);
    CodedOutputData output(data.getPtr(), size);
    output.writeInt32(value);
​
    return setDataForKey(move(data), key);
}
​
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();
​
#ifndef MMKV_DISABLE_CRYPT
    if (m_crypter) {
        if (isDataHolder) {
            auto sizeNeededForData = pbRawVarint32Size((uint32_t) data.length()) + data.length();
            if (!KeyValueHolderCrypt::isValueStoredAsOffset(sizeNeededForData)) {
                data = MiniPBCoder::encodeDataWithObject(data);//将value构造出一个Protobuf数据对象
                isDataHolder = false;
            }
        }
        auto itr = m_dicCrypt->find(key);
        if (itr != m_dicCrypt->end()) {
#    ifdef MMKV_APPLE
            auto ret = appendDataWithKey(data, key, itr->second, isDataHolder);
#    else
            //存数据逻辑
            auto ret = appendDataWithKey(data, key, isDataHolder);
#    endif
            if (!ret.first) {
                return false;
            }
            if (KeyValueHolderCrypt::isValueStoredAsOffset(ret.second.valueSize)) {
                KeyValueHolderCrypt kvHolder(ret.second.keySize, ret.second.valueSize, ret.second.offset);
                memcpy(&kvHolder.cryptStatus, &t_status, sizeof(t_status));
                itr->second = move(kvHolder);
            } else {
                itr->second = KeyValueHolderCrypt(move(data));
            }
        } else {
            auto ret = appendDataWithKey(data, key, isDataHolder);
            if (!ret.first) {
                return false;
            }
            if (KeyValueHolderCrypt::isValueStoredAsOffset(ret.second.valueSize)) {
                auto r = m_dicCrypt->emplace(
                    key, KeyValueHolderCrypt(ret.second.keySize, ret.second.valueSize, ret.second.offset));
                if (r.second) {
                    memcpy(&(r.first->second.cryptStatus), &t_status, sizeof(t_status));
                }
            } else {
                m_dicCrypt->emplace(key, KeyValueHolderCrypt(move(data)));
            }
        }
    } else
#endif // MMKV_DISABLE_CRYPT
    {
       //在这里判断m_dic是否已经存在该Key,有就替换,没就添加
        auto itr = m_dic->find(key);
        if (itr != m_dic->end()) {
            auto ret = appendDataWithKey(data, itr->second, isDataHolder);
            if (!ret.first) {
                return false;
            }
            itr->second = std::move(ret.second);
        } else {
            auto ret = appendDataWithKey(data, key, isDataHolder);
            if (!ret.first) {
                return false;
            }
            m_dic->emplace(key, std::move(ret.second));//和insert类似 只不过emplace 最大的作用是避免产生不必要的临时变量
        }
    }
    m_hasFullWriteback = false;
#ifdef MMKV_APPLE
    [key retain];
#endif
    return true;
}
//将该对象添加到内存里
//pair是将2个数据组合成一个数据,当需要这样的需求时就可以使用pair,
//如stl中的map就是将key和value放在一起来保存。另一个应用是,当一个函数需要返回2个数据的时候,可以选择pair。 
//pair的实现是一个结构体,主要的两个成员变量是first second 因为是使用struct不是class,所以可以直接使用pair的成员变量。
KVHolderRet_t MMKV::appendDataWithKey(const MMBuffer &data, const KeyValueHolder &kvHolder, bool isDataHolder) {
    SCOPED_LOCK(m_exclusiveProcessLock);
​
    uint32_t keyLength = kvHolder.keySize;
    // size needed to encode the key
    size_t rawKeySize = keyLength + pbRawVarint32Size(keyLength);
    
    // ensureMemorySize() might change kvHolder.offset, so have to do it early
    {
        auto valueLength = static_cast<uint32_t>(data.length());
        if (isDataHolder) {
            valueLength += pbRawVarint32Size(valueLength);
        }
        auto size = rawKeySize + valueLength + pbRawVarint32Size(valueLength);//计算存储的数据内存大小
        bool hasEnoughSize = ensureMemorySize(size);//目前内存页是否足够存储
        if (!hasEnoughSize) {
            return make_pair(false, KeyValueHolder());
        }
    }
    auto basePtr = (uint8_t *) m_file->getMemory() + Fixed32Size;
    MMBuffer keyData(basePtr + kvHolder.offset, rawKeySize, MMBufferNoCopy);
    
    return doAppendDataWithKey(data, keyData, isDataHolder, keyLength);//添加到内存里
}
//扩容
bool MMKV::ensureMemorySize(size_t newSize) {
    if (!isFileValid()) {
        MMKVWarning("[%s] file not valid", m_mmapID.c_str());
        return false;
    }
​
    if (newSize >= m_output->spaceLeft() || (m_crypter ? m_dicCrypt->empty() : m_dic->empty())) {
        // try a full rewrite to make space
        auto fileSize = m_file->getFileSize();
        auto preparedData = m_crypter ? prepareEncode(*m_dicCrypt) : prepareEncode(*m_dic);
        auto sizeOfDic = preparedData.second;
        size_t lenNeeded = sizeOfDic + Fixed32Size + newSize;
        size_t dicCount = m_crypter ? m_dicCrypt->size() : m_dic->size();
        size_t avgItemSize = lenNeeded / std::max<size_t>(1, dicCount);
        size_t futureUsage = avgItemSize * std::max<size_t>(8, (dicCount + 1) / 2);
        // 1. no space for a full rewrite, double it
        // 2. or space is not large enough for future usage, double it to avoid frequently full rewrite
        //如果文件空间小于需要的空间长度会进行扩容,每次空间的扩容为原来的两倍
        if (lenNeeded >= fileSize || (lenNeeded + futureUsage) >= fileSize) {
            size_t oldSize = fileSize;
            do {
                //进行扩容
                fileSize *= 2;
            } while (lenNeeded + futureUsage >= fileSize);
            MMKVInfo("extending [%s] file size from %zu to %zu, incoming size:%zu, future usage:%zu", m_mmapID.c_str(),
                     oldSize, fileSize, newSize, futureUsage);
    
            // if we can't extend size, rollback to old state
            //无法扩容判断  
            if (!m_file->truncate(fileSize)) {
                return false;
            }
    
            // check if we fail to make more space
            //扩容失败
            if (!isFileValid()) {
                MMKVWarning("[%s] file not valid", m_mmapID.c_str());
                return false;
            }
        }
        return doFullWriteBack(move(preparedData), nullptr);
    }
    return true;
}

数据读取 mmkv.get

读取相对写入就跟简单了,直接从映射内存页里将数据查找取出即可。

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;
}
​
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;
}
​

以上为MMKV与mmap的解析;有关更多Android的技术进阶;各位可以参考《Android核心技术手册》这个文档。点击查看

mmkv产生的SP存在的问题

通过对原理啊的了解,我们会发现这样做有很多问题。总一下,主要有如下几个问题:

  • 1.最终写入XML文件实用的是IO操作,IO操作需要两次拷贝,效率是比较低的。(原因自行百度,这里就不再赘述了)
  • 2.实用XML格式进行存储,并且全部以字符串的形式进行保存,浪费存储空间。比如value=“469068865”。需要占用17个字节,utf-8一个英文字符占用1个字节,则存储该值需要17个字节。
  • 3.每次编辑时,都需要对文件进行全量的写入操作。因为每次都是对完整的数据Map进行写入操作,哪怕只修改了一个值。这样做无疑是极大的浪费。
  • 4.SP虽然支持多进程访问,但是多进程的读取是相当不安全的,因为进程间内存不能共享,而SP的多进程是每个进程一个对象进行操作。所以我们安全的使用方式仍然是使用一个进程去读取,并提供ContentProvider的方式供其它进程访问或者增加文件锁的方式,这样做无疑增加了我们使用复杂度。
  • 5.线程阻塞问题。上面我们看到,只有全部加载完xml中的内容后,getString的函数才能继续往下执行。所以线程会被阻塞。

解决办法:

  • 实现高效的文件操作
  • 实现更精简的数据格式
  • 实现更优的数据更新方式
  • 解决多进程一致性
  • 线程阻塞问题

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/356126.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

海量数据相似数据查询方法

1、海量文本常见 海量文本场景&#xff0c;如何寻找一个doc的topn相似doc&#xff0c;一般存在2个问题&#xff0c; 1)、两两对比时间o(n^2) 2)、高维向量比较比较耗时。 文本集可以看成(doc,word)稀疏矩阵&#xff0c;一般常见的方法是构建到排索引&#xff0c;然后进行归并…

论文投稿指南——中文核心期刊推荐(社会学)

【前言】 &#x1f680; 想发论文怎么办&#xff1f;手把手教你论文如何投稿&#xff01;那么&#xff0c;首先要搞懂投稿目标——论文期刊 &#x1f384; 在期刊论文的分布中&#xff0c;存在一种普遍现象&#xff1a;即对于某一特定的学科或专业来说&#xff0c;少数期刊所含…

【论文精读】DeepWalk: Online Learning of Social Representations

DeepWalk: Online Learning of Social Representations 本文是我参加Datawhale的CS224W图机器学习时的笔记&#xff0c;第一次学习图机器学习&#xff0c;对DeepWalk这篇开山之作的理解。 论文的三位作者均来自纽约州立大学石溪分校&#xff0c;杨振宁和丘成桐也曾在此教学。 …

JavaScript内存泄露和垃圾回收机制

1、是什么&#xff1f;内存泄露&#xff08;Memory leak&#xff09;是在计算机科学中&#xff0c;由于疏忽或错误造成程序未能释放已经不再使用的内存。并非指内存在物理上的消失&#xff0c;而是应用程序分配某段内存后&#xff0c;由于设计错误&#xff0c;导致在释放该段内…

Spring Boot 项目介绍

Spring Boot 项目介绍 作为学习过 Java 的软件开发者&#xff0c;相信都知道 Spring 这一伟大的框架&#xff0c;它所拥有的强大功能之一就是可以集成各种开源软件。但随着互联网的高速发展&#xff0c;各种框架层出不穷&#xff0c;这就对系统架构的灵活性、扩展性、可伸缩性…

【项目精选】基于JSP物流信息网(论文+源码+视频)

点击下载源码 近年来&#xff0c;随着时代的进步&#xff0c;社会随之不断发展&#xff0c;经济也快速发展起来了&#xff0c;人民的消费水平在不断地提高&#xff0c;平常的实体店消费已经不能满足人们的需求&#xff1b;在者&#xff0c;互联网技术的不断发展也为电子商务的兴…

CSS Houdini

前言 最近看了几篇文章&#xff0c;是关于 CSS Houdini 的。作为一个前端搬砖的还真不知道这玩意&#xff0c;虽然不知道的东西挺多的&#xff0c;但是这玩意有点高大上啊。 Houdini 是一组底层 API&#xff0c;它们公开了 CSS 引擎的各个部分&#xff0c;从而使开发人员能够通…

Matlab搭建AlexNet实现手写数字识别

Matlab搭建AlexNet实现手写数字识别 个人博客地址 文章目录Matlab搭建AlexNet实现手写数字识别环境内容步骤准备MNIST数据集数据预处理定义网络模型定义训练超参数网络训练和预测代码下载环境 Matlab 2020aWindows10 内容 使用Matlab对MNIST数据集进行预处理&#xff0c;搭建…

基于Spring Boot框架的人事管理系统的设计与实现(程序+详细文档)

大家好✌&#xff01;我是CZ淡陌。这里为大家分享优质的实战项目&#xff0c;本人在Java毕业设计领域有多年的经验&#xff0c;陆续会更新更多优质的Java实战项目&#xff0c;希望你能有所收获&#xff0c;少走一些弯路&#xff01; &#x1f345;更多优质项目&#x1f447;&am…

面向六部十层电梯群控算法的研究

面向六部十层电梯群控算法的研究 赵大权&#xff0c;张翔宇 &#xff08;晋中学院 机械学院&#xff0c;山西 晋中 030619&#xff09; 摘 要&#xff1a; 在商业大楼和高层写字楼里&#xff0c;基于可编程逻辑控制器&#xff08;PLC&#xff09;对电梯运行进行控制是当前主流…

【LeetCode】剑指 Offer(4)

目录 写在前面&#xff1a; 题目&#xff1a;剑指 Offer 10- I. 斐波那契数列 - 力扣&#xff08;Leetcode&#xff09; 题目的接口&#xff1a; 解题思路&#xff1a; 代码&#xff1a; 过啦&#xff01;&#xff01;&#xff01; 题目&#xff1a;剑指 Offer 10- II. …

河南工程学院2.17蓝桥杯培训

乘法口诀数列&#xff1a;https://www.acwing.com/problem/content/3466/ 剪绳子&#xff1a;https://www.acwing.com/problem/content/68Sin SinSine之舞&#xff1a;http://lx.lanqiao.cn/problem.page?gpidD5272 数列&#xff1a;https://www.acwing.com/problem/content/…

【郭东白架构课 模块一:生存法则】13|法则六:如何鉴别文化环境是否有利于架构师的生存?

你好&#xff0c;我是郭东白。 架构师通常并不管理团队&#xff0c;而是管理架构活动。更准确地说&#xff0c;是定义和引导架构活动。因为每个参与架构活动的个体&#xff0c;都有各自工作的优先级和汇报关系。因此在没有管理、考核和激励等手段的保障下&#xff0c;唯一能够…

Chrome 又不支持 HTTP/2 网站的原因

导读昨晚偶尔清理 Chrome 插件时发现我的 “HTTP/2 and SPDY indicator”插件好像好久没亮了。这个插件在你访问到一个支持 HTTP/2 &#xff08;或之前的 SPDY 协议&#xff09;的网站时会点亮&#xff0c;而我明明记得之前专门让 https://linux.cn/ 支持了 HTTP/2 。 我的第一…

软考高级-信息系统管理师之整体管理(最新版)

整体管理 1、项目整体管理概述2、制定项目章程(选择,案例,论文)制定项目章程过程制定项目章程的依据1、协议2.项目工作说明书:3、商业论证4、事业环境因素包括,但不限于如下事项。5、组织过程资产:项目选择方法项目启动会议项目目标引导技术3、制订项目管理计划(选择)项目管…

MakeFile编写 使用

目录 1、基本格式如下&#xff1a;2、GCC编译过程3、Makefile具体流程可参考下图&#xff1a;4、Makefile变量解析![在这里插入图片描述](https://img-blog.csdnimg.cn/50fdafadef79400abea65b64a12f8ec8.png)5、实例项目目录5.1 使用g直接编译5.2 Version 15.3 Version 25.4 V…

[oeasy]python0086_ASCII_出现背景_1963年_DEC_PDP系列主机_VT系列终端

编码进化 回忆上次内容 上次 回顾了 字符编码的新陈代谢 ibm 曾经的EBCDIC 由于 字符不连续导致 后续 出现无数问题 随着 网络的发展 数据交换的 需要原来的小隐患现在 产生了 巨大问题 Bemer 联合各方巨头 想要推出 字符连续的编码集 这新编码集 具体长什么样 呢&#xff1…

springmvc绿植培养交流平台java的ssm设计与实现

本绿植培养交流平台设计与实现以SSM作为框架&#xff0c;B/S模式以及MySql作为后台运行的数据库。本系统主要包括以下功能模块&#xff1a;个人中心、用户管理、品种类型管理、绿植信息管理、视频分类管理、视频信息管理、经验交流、系统管理等模块&#xff0c;通过这些模块的实…

Java之动态规划之机器人移动

目录 0.动态规划问题 一.不同路径 1.题目描述 2.问题分析 3.代码实现 二.不同路径 II 1.题目描述 2.问题分析 3.代码实现 三.机器人双向走路 1.题目描述 2.问题分析 3.代码实现 0.动态规划问题 动态规划(Dynamic Programming)算法的核心思想是:将大问题划分为小问…

九龙证券|连续七周获加仓,四大行业成“香饽饽”!

本周17个申万职业北上资金持股量环比增加。 北上资金抢筹铝业龙头 本周A股商场全体冲高回落&#xff0c;沪指收跌1.12%&#xff0c;深成指跌2.18%&#xff0c;创业板指跌3.76%。北上资金周内小幅净流入。在大盘体现较差的周四周五&#xff0c;北上资金别离逆市回流67.94亿元、…