mmkv 原理解析

mmkv 原理解析

本文通过对mmkv的原理,和源码分析,深入剖析mmkv的功能实现。

mmkv是什么?

 首先,在mmkv开源项目中对MMKV是这么描述的,MMKV 是基于 mmap 内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。从 2015 年中至今在微信上使用,其性能和稳定性经过了时间的验证。
 由上我们可以大致总结一下mmkv的核心也是我们本文会着重介绍的知识和内容。
一、基于 mmap 内存映射 二、使用 protobuf 实现序列化。三、源码解读。

将 MMKV 和 SharedPreferences、SQLite 进行对比, 重复读写操作 1k 次。相关测试代码在 Android/MMKV/mmkvdemo/。结果如下图表。
单进程性能
可见,MMKV 在写入性能上远远超越 SharedPreferences & SQLite,在读取性能上也有相近或超越的表现。
在这里插入图片描述
(测试机器是 Pixel 2 XL 64G,Android 8.1,每组操作重复 1k 次,时间单位是 ms。)

多进程性能
可见,MMKV 无论是在写入性能还是在读取性能,都远远超越 MultiProcessSharedPreferences & SQLite & SQLite, MMKV 在 Android 多进程 key-value 存储组件上是不二之选。
在这里插入图片描述
(测试机器是 Pixel 2 XL 64G,Android 8.1,每组操作重复 1k 次,时间单位是 ms。)

mmkv产生的原因(SP的几个问题)

MMKV的出现其实是为了解决SharedPreferences的一些问题,微信团队希望以此来代替SharedPreferences,目前在Android中,对于经常使用的快速本地化存储,大部分人往往会选择SharedPreferences来作为存储方式, 作为Android库中自带的存储方式,SharePreferences在使用方式上还是很便捷的,但是也往往存在以下的一些问题。

1、通过 getSharedPreferences 可以获取 SP 实例,从首次初始化到读到数据会存在延迟,因为读文件的操作阻塞调用的线程直到文件读取完毕,如果在主线程调用,可能会对 UI 流畅度造成影响。(线程阻塞)
2、虽然支持设置 MODE_MULTI_PROCESS 标志位,但是跨进程共享 SP 存在很多问题,所以不建议使用该模式。(文件跨进程共享)
3、将数据写入文件需要将数据拷贝两次,再写入到文件中,如果数据量过大,也会有很大的性能损耗。(二次写入)

 Tips: commit 会在调用者线程同步执行写文件,返回写入结果;apply 将写文件的操作异步执行,没有返回值。可以根据具体情况选择性使用,推荐使用 apply。
下图为SP的IO存储方式
Sp存储方式
  众所周知,Android是基于Linux系统的,而在Linux中虚拟内存被操作系统划分成两块:用户空间和内核空间,用户空间是用户程序代码运行的地方,内核空间是内核代码运行的地方。为了安全,它们是隔离的,即使用户的程序崩溃了,内核也不受影响。
在上图中我们可以知道,SP在IO存储会经历这么几个步骤,
1、通过内核write方法,告诉内核需要写入数据的开始地址与长度
2、内核将数据拷贝到内核缓存
3、由操作系统调用,将数据拷贝到磁盘,完成写入

那么整体数据到最后存储的时候就会经历两次的拷贝,相对于一次写入在速度上就会显得较慢一些。

Q:那读取速度呢?
A:读取的时候两者都是在初始化时将数据保存在了一个map中,从内存中读取,所以两者的读取速度是没什么分别的。

MMKV的原理预知

为了应对上述的一些问题,mmkv主要在以下几个方面进行了设计

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

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 0010010 1100  000 0010//移去标识位
如下:
000 0010  010 1100000 0010 ++ 010 1100 //拼接10 0101100256 + 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;
}

参考内容

Protobuf协议格式详解

Google 的开源技术protobuf 简介与例子

详解通信数据协议ProtoBuf

protobuf官方文档

MMKVGit官方文档

深度分析mmap:是什么 为什么 怎么用 性能总结

iOS进阶——微信开源存储框架MMKV(一)

Android 存储优化 —— MMKV 集成与原理

  • 2
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值