mmkv框架源码浅析(下)

由于篇幅过长,就分成三篇来分析mmkv这块内容,虽然代码量不多,但集成了一些基础知识,掌握基本知识,学其他的会更快些。

八) 使用文件锁来调度多进程通过共享内存协作

mmkv下面有一些源文件,其中ScopedLock.hpp通过ARII手段来通过构造时lock,离开函数作用域后析构unlock,当然实现中,拷贝构造和赋值函数都用delete修饰;

native-bridge.cpp应该是C++和java之间的通道,在java层面调用C++的逻辑;

ThreadLock.h/ThreadLock.cpp是个封装pthread_mutex_t的锁,其中在初始化锁时有个属性PTHREAD_MUTEX_RECURSIVE,表示可递归;

PBUtility.h/PBUtility.cpp如其名,是一些基本的接口类,有些是c++11的知识点,比如constexpr可用于在编译期就计算出来的表达式;类型转换如leveldb中的Varint类型:

 23 uint32_t pbRawVarint32Size(int32_t value) {
 24     if ((value & (0xffffffff << 7)) == 0) {
 25         return 1;
 26     } else if ((value & (0xffffffff << 14)) == 0) {
 27         return 2;
 28     } else if ((value & (0xffffffff << 21)) == 0) {
 29         return 3;
 30     } else if ((value & (0xffffffff << 28)) == 0) {
 31         return 4;
 32     }
 33     return 5;
 34 }

上面实现算出一个int32_t用Varint类型来表示,占用多少字节,每个字节的最高位不存储数据,表示是否还有后续内容;

MMBuffer.h/MMBuffer.cpp封装了void *ptrsize_t size的缓冲区,和leveldb中的slice有点区别,区别是否是对源地址内容重新拷贝一份而不简单的指向同一个地址,但与redis中的sds有点类似,不过后者是二进制安全的,分配空间策略也与vector类似;MMBuffer.cpp中也有移动拷贝和赋值相关的接口;

InterProcessLock.h/InterProcessLock.cpp 改造了文件锁,加了类型和读写引用计数,实现原理参考之前的内容;
MMKVLog.h是日志相关的接口实现;
aes目录下有不同的加解密实现;

MMKVMetaInfo.hpp是用于存放在共享内存中的元数据:

 28 struct MMKVMetaInfo {
 29     uint32_t m_crcDigest = 0;
 30     uint32_t m_version = 1;  //版本号
 31     uint32_t m_sequence = 0; // full write-back count
 32     //more code...
 42 };

这个结构是在多进程同步数据的时候,用于检查内存地址,大小是否发生改变,具体使用和实现在下面说明;

MmapedFile.h/MmapedFile.cpp封装了共享内存映mmap和munmap相关的实现;这里有映射文件和/dev/shm,分析的时候会以文件为例,因为有些注意点在里面,可以参考这里认真分析mmap:是什么 为什么 怎么用

 40 class MmapedFile {
 41     std::string m_name;
 42     int m_fd;
 43     void *m_segmentPtr;
 44     size_t m_segmentSize;
 66 };

 36 MmapedFile::MmapedFile(const std::string &path, size_t size, bool fileType)
 37     : m_name(path), m_fd(-1), m_segmentPtr(nullptr),m_segmentSize(0), m_fileType(fileType) {
 38     if (m_fileType == MMAP_FILE) {
 39         m_fd = open(m_name.c_str(), O_RDWR | O_CREAT, S_IRWXU);
 40         if (m_fd < 0) {
 42         } else {
 43             struct stat st = {};
 44             if (fstat(m_fd, &st) != -1) {
 45                 m_segmentSize = static_cast<size_t>(st.st_size);
 46             }
 47             if (m_segmentSize < DEFAULT_MMAP_SIZE) {
 48                 m_segmentSize = static_cast<size_t>(DEFAULT_MMAP_SIZE);
 49                 if (ftruncate(m_fd, m_segmentSize) != 0 || !zeroFillFile(m_fd, 0, m_segmentSize)) {
 51                               m_segmentSize, strerror(errno));
 52                     close(m_fd);
 53                     m_fd = -1;
 54                     removeFile(m_name);
 55                     return;
 56                 }
 57             }
 58             m_segmentPtr =
 59                 (char *) mmap(nullptr, m_segmentSize, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0);
 60             if (m_segmentPtr == MAP_FAILED) {
 62                 close(m_fd);
 63                 m_fd = -1;
 64                 m_segmentPtr = nullptr;
 65             }
 66         }
 67     } else {
 68         m_fd = open(ASHMEM_NAME_DEF, O_RDWR);
            //more code...
 91     }
 92 }

121 MmapedFile::~MmapedFile() {
122     if (m_segmentPtr != MAP_FAILED && m_segmentPtr != nullptr) {
123         munmap(m_segmentPtr, m_segmentSize);
124         m_segmentPtr = nullptr;
125     }
126     if (m_fd >= 0) {
127         close(m_fd);
128         m_fd = -1;
129     }
130 }

引用“mmap映射区域大小必须是物理页大小(page_size)的整倍数(32位系统中通常是4k字节)。原因是,内存的最小粒度是页,而进程虚拟地址空间和内存的映射也是以页为单位。为了匹配内存的操作,mmap从磁盘到虚拟地址空间的映射也必须是页。”

CodedInputData.h/CodedInputData.cpp封装了数据的读,可以有一定的格式:

 28 class CodedInputData {
 29     uint8_t *m_ptr;
 30     int32_t m_size;
 31     int32_t m_position;
 63 };

 28 CodedInputData::CodedInputData(const void *oData, int32_t length)
 29     : m_ptr((uint8_t *) oData), m_size(length), m_position(0) {
 30     assert(m_ptr); //其实从oData中读数据
 31 }

 87 MMBuffer CodedInputData::readData() {
 88     int32_t size = this->readRawVarint32();
 89     if (size < 0) {
 91         return MMBuffer(0);
 92     }
 93 
 94     if (size <= m_size - m_position) {
 95         MMBuffer data(((int8_t *) m_ptr) + m_position, size);
 96         m_position += size;
 97         return data;
 98     } else {
100         return MMBuffer(0);
101     }
102 }

162 int8_t CodedInputData::readRawByte() {
163     if (m_position == m_size) {
165         return 0;
166     }
167     int8_t *bytes = (int8_t *) m_ptr;
168     return bytes[m_position++];
169 }

CodedOutputData.h/CodedOutputData.cpp是封装了写数据:

 28 class CodedOutputData {
 29     uint8_t *m_ptr;
 30     size_t m_size;
 31     int32_t m_position;
 67 };

 27 CodedOutputData::CodedOutputData(void *ptr, size_t len)
 28     : m_ptr((uint8_t *) ptr), m_size(len), m_position(0) {
 29     assert(m_ptr); //写入ptr
 30 }
124 void CodedOutputData::writeRawLittleEndian32(int32_t value) {
125     this->writeRawByte(static_cast<uint8_t>((value) &0xff));
126     this->writeRawByte(static_cast<uint8_t>((value >> 8) & 0xff));
127     this->writeRawByte(static_cast<uint8_t>((value >> 16) & 0xff));
128     this->writeRawByte(static_cast<uint8_t>((value >> 24) & 0xff));
129 }  //把一个int32_t以小端格式写进m_ptr中

100 void CodedOutputData::writeRawVarint32(int32_t value) {
101     while (true) {
102         if ((value & ~0x7f) == 0) {
103             this->writeRawByte(static_cast<uint8_t>(value));
104             return;
105         } else {
106             this->writeRawByte(static_cast<uint8_t>((value & 0x7F) | 0x80)); //先取七位并或上第八位,表示后面还有数据
107             value = logicalRightShift32(value, 7); //右移掉低七位
108         }
109     }
110 }  //以Varint格式写
 85 void CodedOutputData::writeRawByte(uint8_t value) {
 86     if (m_position == m_size) {
 88         return;
 89     }
 90 
 91     m_ptr[m_position++] = value;
 92 }

以上输入输出的实现还是比较好理解;但是需要注意在把int32_t有符号的类型转换成Varint类型时,移位可能补上符号位,这里需要特别处理下,相关的有无符号转换表示等基础知识可以参考下《深入理解计算机系统第二章节》:

 69 static inline uint32_t Int32ToUInt32(int32_t v) {
 70     Converter<int32_t, uint32_t> converter;
 71     converter.first = v;
 72     return converter.second;
 73 }

 26 template <typename T, typename P>
 27 union Converter {
 28     static_assert(sizeof(T) == sizeof(P), "size not match");
 29     T first;
 30     P second;
 31 };

MiniPBCoder.h/MiniPBCoder.cpp用于Encode/decode处理,举例对string进行Encode/decode:

232 string MiniPBCoder::decodeOneString() {
233     return m_inputData->readString();
234 }
235 
273 string MiniPBCoder::decodeString(const MMBuffer &oData) {
274     MiniPBCoder oCoder(&oData);
275     return oCoder.decodeOneString();
276 }
174 MMBuffer MiniPBCoder::getEncodeData(const string &str) {
175     m_encodeItems = new vector<PBEncodeItem>();
176     size_t index = prepareObjectForEncode(str);
177     PBEncodeItem *oItem = (index < m_encodeItems->size()) ? &(*m_encodeItems)[index] : nullptr;
178     if (oItem && oItem->compiledSize > 0) {
179         m_outputBuffer = new MMBuffer(oItem->compiledSize);
180         m_outputData = new CodedOutputData(m_outputBuffer->getPtr(), m_outputBuffer->length());
181 
182         writeRootObject();
183     }
184 
185     return move(*m_outputBuffer);
186 }

 90 size_t MiniPBCoder::prepareObjectForEncode(const string &str) {
 91     m_encodeItems->push_back(PBEncodeItem());
 92     PBEncodeItem *encodeItem = &(m_encodeItems->back());
 93     size_t index = m_encodeItems->size() - 1;
 94     {
 95         encodeItem->type = PBEncodeItemType_String;
 96         encodeItem->value.strValue = &str;
 97         encodeItem->valueSize = static_cast<int32_t>(str.size());
 98     }
 99     encodeItem->compiledSize = pbRawVarint32Size(encodeItem->valueSize) + encodeItem->valueSize; //数据大小占用字节数加内存本身占用的内存
100 
101     return index;
102 }

 66 void MiniPBCoder::writeRootObject() {
 67     for (size_t index = 0, total = m_encodeItems->size(); index < total; index++) {
 68         PBEncodeItem *encodeItem = &(*m_encodeItems)[index];
 69         switch (encodeItem->type) {
 70             case PBEncodeItemType_String: {
 71                 m_outputData->writeString(*(encodeItem->value.strValue));
 72                 break;  //依次把string item 写进m_outputData
 73             }
 74             case PBEncodeItemType_Data: {
 75                 m_outputData->writeData(*(encodeItem->value.bufferValue));
 76                 break;
 77             }
 78             case PBEncodeItemType_Container: {
 79                 m_outputData->writeRawVarint32(encodeItem->valueSize);
 80                 break;
 81             }
 82             case PBEncodeItemType_None: {
 84                 break;
 85             }
 86         }
 87     }
 88 }

以上所介绍的都是基本的,主要的逻辑在MMKV.h/MMKV.cpp中,剩下的部分会分析重点部分,然后以一个读和写操作来分析工作流程,其他的跳过。
以下是类的声明,数据成员如下:

 44 class MMKV {
 45     std::unordered_map<std::string, MMBuffer> m_dic;
 46     std::string m_mmapID;  //共享内存id
 47     std::string m_path; //文件名,以它来举例
 48     std::string m_crcPath;
 49     int m_fd;
 50     char *m_ptr;
 51     size_t m_size;
 52     size_t m_actualSize;
 53     CodedOutputData *m_output;
 54     MmapedFile *m_ashmemFile;
 55 
 56     bool m_needLoadFromFile;
 57 
 58     uint32_t m_crcDigest;
 59     MmapedFile m_metaFile;
 60     MMKVMetaInfo m_metaInfo;
 61 
 62     AESCrypt *m_crypter; //加解密相关
 63 
 64     ThreadLock m_lock;  //支持递归加锁
 65     FileLock m_fileLock;  //用于初始化读写锁的文件锁
 66     InterProcessLock m_sharedProcessLock; //读锁
 67     InterProcessLock m_exclusiveProcessLock; //写锁
 217 };

构造:

  59 MMKV::MMKV(const std::string &mmapID, int size, MMKVMode mode, string *cryptKey)
  60     : m_mmapID(mmapID)
  61     , m_path(mappedKVPathWithID(m_mmapID, mode))
  62     , m_crcPath(crcPathWithID(m_mmapID, mode))
  63     , m_metaFile(m_crcPath, DEFAULT_MMAP_SIZE, (mode & MMKV_ASHMEM) ? MMAP_ASHMEM : MMAP_FILE)
  64     , m_crypter(nullptr)
  65     , m_fileLock(m_metaFile.getFd())
  66     , m_sharedProcessLock(&m_fileLock, SharedLockType)
  67     , m_exclusiveProcessLock(&m_fileLock, ExclusiveLockType)
  68     , m_isInterProcess((mode & MMKV_MULTI_PROCESS) != 0) //是否是多进程的
  69     , m_isAshmem((mode & MMKV_ASHMEM) != 0) {
  70     m_fd = -1;
  71     m_ptr = nullptr;
  72     m_size = 0;
  73     m_actualSize = 0;
  74     m_output = nullptr;
  75    
  76     if (m_isAshmem) {
  79     } else {
  80         m_ashmemFile = nullptr;
  81     }
  82                    
  87     m_needLoadFromFile = true;
  88 
  89     m_crcDigest = 0;
  90 
  91     m_sharedProcessLock.m_enable = m_isInterProcess;//文件锁是否用于多进程,这样加解锁是否要真正进行
  92     m_exclusiveProcessLock.m_enable = m_isInterProcess;//同上  93 
  94     // sensitive zone
  95     {
  96         SCOPEDLOCK(m_sharedProcessLock); //加读锁
  97         loadFromFile(); //加载数据
  98     }
  99 }

简化起见,不考虑从/dev/shm中映射,不考虑cryptKey相关的逻辑,把里面的相关代码给省略,对m_metaFile区域初始化文件锁,因为在共享内存中,可能有多个进程操作。

加载数据:

 267 void MMKV::loadFromFile() {
 268     if (m_isAshmem) {
 270         return;
 271     }
 273     m_metaInfo.read(m_metaFile.getMemory()); //从共享内存中读元数据
 274 
 275     m_fd = open(m_path.c_str(), O_RDWR | O_CREAT, S_IRWXU);
 278     } else {   
 279         m_size = 0;
 280         struct stat st = {0};
 281         if (fstat(m_fd, &st) != -1) {
 282             m_size = static_cast<size_t>(st.st_size);
 283         }      
 284         // round up to (n * pagesize)
 285         if (m_size < DEFAULT_MMAP_SIZE || (m_size % DEFAULT_MMAP_SIZE != 0)) {
 286             size_t oldSize = m_size;
 287             m_size = ((m_size / DEFAULT_MMAP_SIZE) + 1) * DEFAULT_MMAP_SIZE;
 288             if (ftruncate(m_fd, m_size) != 0) { 
 291                 m_size = static_cast<size_t>(st.st_size);
 292             }
 293             zeroFillFile(m_fd, oldSize, m_size - oldSize); 
 294         }
 295         m_ptr = (char *) mmap(nullptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0); //以MAP_SHARED进行映射
 296         if (m_ptr == MAP_FAILED) {
 298         } else {
 299             memcpy(&m_actualSize, m_ptr, Fixed32Size);
 302             bool loaded = false;
 303             if (m_actualSize > 0) {
 304                 if (m_actualSize < m_size && m_actualSize + Fixed32Size <= m_size) {
 305                     if (checkFileCRCValid()) {
 308                         MMBuffer inputBuffer(m_ptr + Fixed32Size, m_actualSize, MMBufferNoCopy);
 309                         if (m_crypter) {
 310                             decryptBuffer(*m_crypter, inputBuffer);
 311                         }
 312                         m_dic = MiniPBCoder::decodeMap(inputBuffer);
 313                         m_output = new CodedOutputData(m_ptr + Fixed32Size + m_actualSize,
 314                                                        m_size - Fixed32Size - m_actualSize);
 315                         loaded = true;
 316                     }
 317                 }
 318             }

代码行273主要是从共享内存中读元数据,里面含有字段信息:m_crcDigest/m_version/m_sequence,具体作用在后面说。
279到295开始映射,并根据页大小来,然后从oldSize开始初始化m_size - oldSize个字节内容为0。当然一开始没有文件的时候,初始化一页都为0的内容,然后进行映射,此时进行读写m_ptr时就相当于读写文件。
一开始文件中啥都没有,所以不会执行303〜315逻辑:

 319             if (!loaded) {
 320                 SCOPEDLOCK(m_exclusiveProcessLock);
 321 
 322                 if (m_actualSize > 0) {
 323                     writeAcutalSize(0);
 324                 }
 325                 m_output = new CodedOutputData(m_ptr + Fixed32Size, m_size - Fixed32Size);
 326                 recaculateCRCDigest();
 327             }

然后设置m_ptr开始的Fixed32Size个字节偏移量处开始写,然后重新计算签名recaculateCRCDigest,然后写入到共享内存处,这样当其他进入进行写的时候,可能会做些额外处理:

 890 void MMKV::updateCRCDigest(const uint8_t *ptr, size_t length, bool increaseSequence) {
 891     if (!ptr) {
 892         return;
 893     }
 894     m_crcDigest = (uint32_t) crc32(m_crcDigest, ptr, (uint32_t) length);
 895 
 896     void *crcPtr = m_metaFile.getMemory();
 897     if (crcPtr == nullptr || crcPtr == MAP_FAILED) {
 898         return;
 899     }
 900 
 901     m_metaInfo.m_crcDigest = m_crcDigest;
 902     if (increaseSequence) {
 903         m_metaInfo.m_sequence++;
 904     }
 905     if (m_metaInfo.m_version == 0) {
 906         m_metaInfo.m_version = 1;
 907     }
 908     m_metaInfo.write(crcPtr);
 909 }

当有文件内容时,先进行校验,通过的话则从文件中建立m_dic表并设置输出:

 303             if (m_actualSize > 0) {
 304                 if (m_actualSize < m_size && m_actualSize + Fixed32Size <= m_size) {
 305                     if (checkFileCRCValid()) { //校验
 308                         MMBuffer inputBuffer(m_ptr + Fixed32Size, m_actualSize, MMBufferNoCopy) ; //读文件内容
 309                         if (m_crypter) { //是否加密
 310                             decryptBuffer(*m_crypter, inputBuffer);
 311                         }
 312                         m_dic = MiniPBCoder::decodeMap(inputBuffer);//decode文件中的内容
 313                         m_output = new CodedOutputData(m_ptr + Fixed32Size + m_actualSize,
 314                                                        m_size - Fixed32Size - m_actualSize); //设置输出
 315                         loaded = true;
 316                     }
 317                 }
 318             }

 867 bool MMKV::checkFileCRCValid() {
 868     if (m_ptr && m_ptr != MAP_FAILED) {
 869         constexpr int offset = pbFixed32Size(0);
 870         m_crcDigest =
 871             (uint32_t) crc32(0, (const uint8_t *) m_ptr + offset, (uint32_t) m_actualSize);
 872         m_metaInfo.read(m_metaFile.getMemory());
 873         if (m_crcDigest == m_metaInfo.m_crcDigest) {
 874             return true;
 875         }//从元数据中读出来的值跟文件内容计算值一遍比较
 878     }
 879     return false;
 880 }

以上是加载的过程,从代码实现上看,文件中内容格式是:大小+内容,但是大小只占头4个字节。

以读一个string为例来说明读的过程:

 997 bool MMKV::getStringForKey(const std::string &key, std::string &result) {
 998     if (key.empty()) {
 999         return false;
1000     }
1001     auto &data = getDataForKey(key);
1002     if (data.length() > 0) {
1003         result = MiniPBCoder::decodeString(data); //decode内容
1004         return true;
1005     }
1006     return false;
1007 }

 644 const MMBuffer &MMKV::getDataForKey(const std::string &key) {
 645     SCOPEDLOCK(m_lock);
 646     checkLoadData();  //检查是否要重新load
 647     auto itr = m_dic.find(key);
 648     if (itr != m_dic.end()) {
 649         return itr->second;
 650     }
 651     static MMBuffer nan(0); //没找到,返回个特殊的
 652     return nan;
 653 }

下面这部分算是mmkv的核心设计了,当从文件中加载后m_needLoadFromFilefalse,即不需要在每次查询时是否重新load,但对于多进程来说,可能在load后其他进程更新了内容,所以可能需要重新load:

 437 void MMKV::checkLoadData() {
 438     if (m_needLoadFromFile) {
 439         SCOPEDLOCK(m_sharedProcessLock);
 440 
 441         m_needLoadFromFile = false;
 442         loadFromFile();  //从文件加载,并重置状态
 443         return;
 444     }
 445     if (!m_isInterProcess) {
 446         return;  //单进程直接返回
 447     }
 448 
 449     // TODO: atomic lock m_metaFile?
 450     MMKVMetaInfo metaInfo;
 451     metaInfo.read(m_metaFile.getMemory());
 452     if (m_metaInfo.m_sequence != metaInfo.m_sequence) {
 455         SCOPEDLOCK(m_sharedProcessLock);
 456 
 457         clearMemoryState(); //重置相关数据结构
 458         loadFromFile(); //重新从文件中mmap
 459     } else if (m_metaInfo.m_crcDigest != metaInfo.m_crcDigest) {
 462         SCOPEDLOCK(m_sharedProcessLock);
 463 
 464         size_t fileSize = 0;
 465         if (m_isAshmem) {
 466             fileSize = m_size;
 467         } else {
 468             struct stat st = {0};
 469             if (fstat(m_fd, &st) != -1) {
 470                 fileSize = (size_t) st.st_size;
 471             }
 472         }
 473         if (m_size != fileSize) {
 476             clearMemoryState();
 477             loadFromFile();
 478         } else {
 479             partialLoadFromFile();
 480         }
 481     }
 482 }

代码450〜458,从共享内存中读最新的元数据,然后比较序列号,不相等则内存进行了重整。
如果校验码不一致,则内容发生变化,这里检查[文件]是否大小不同,如果不同则重新加载,如果不是则部分加载,因为此时共内存内存中的数据可能还没有msync到文件中去,只是侧面反映共享内存中的数据增加了:

 389 void MMKV::partialLoadFromFile() {
 390     m_metaInfo.read(m_metaFile.getMemory()); //获取最新的元数据
 391 
 392     size_t oldActualSize = m_actualSize;
 393     memcpy(&m_actualSize, m_ptr, Fixed32Size);
 397     if (m_actualSize > 0) {
 398         if (m_actualSize < m_size && m_actualSize + Fixed32Size <= m_size) {
 399             if (m_actualSize > oldActualSize) {
 400                 size_t bufferSize = m_actualSize - oldActualSize; //部分加载
 401                 MMBuffer inputBuffer(m_ptr + Fixed32Size + oldActualSize, bufferSize,
 402                                      MMBufferNoCopy);
 403                 // incremental update crc digest
 404                 m_crcDigest = (uint32_t) crc32(m_crcDigest, (const uint8_t *) inputBuffer.getPtr(),
 405                                                static_cast<uInt>(inputBuffer.length()));
 406                 if (m_crcDigest == m_metaInfo.m_crcDigest) {
 407                     if (m_crypter) {
 408                         decryptBuffer(*m_crypter, inputBuffer);
 409                     }
 410                     auto dic = MiniPBCoder::decodeMap(inputBuffer, bufferSize);
 411                     for (auto &itr : dic) {
 412                         //m_dic[itr.first] = std::move(itr.second);
 413                         auto target = m_dic.find(itr.first);
 414                         if (target == m_dic.end()) {
 415                             m_dic.emplace(itr.first, std::move(itr.second));
 416                         } else {
 417                             target->second = std::move(itr.second);
 418                         }
 419                     } //调整m_dic
 420                     m_output->seek(bufferSize);
 424                     return;
 425                 } else {
 428                 }
 429             }
 430         }
 431     }
 432     //到这里,说明上面部分load出错,进行清理状态并重新mmap
 433     clearMemoryState();
 434     loadFromFile();
 435 }

写及内存变化:

 913 bool MMKV::setStringForKey(const std::string &value, const std::string &key) {
 914     if (key.empty()) {
 915         return false;
 916     }
 917     auto data = MiniPBCoder::encodeDataWithObject(value);
 918     return setDataForKey(std::move(data), key);
 919 }

 655 bool MMKV::setDataForKey(MMBuffer &&data, const std::string &key) {
 656     if (data.length() == 0 || key.empty()) {
 657         return false;
 658     }
 659     SCOPEDLOCK(m_lock);
 660     SCOPEDLOCK(m_exclusiveProcessLock);
 661     checkLoadData();
 662 
 663     // m_dic[key] = std::move(data);
 664     auto itr = m_dic.find(key);
 665     if (itr == m_dic.end()) {
 666         itr = m_dic.emplace(key, std::move(data)).first;
 667     } else {
 668         itr->second = std::move(data);
 669     }
 670 
 671     return appendDataWithKey(itr->second, key);
 672 }

 688 bool MMKV::appendDataWithKey(const MMBuffer &data, const std::string &key) {
 689     size_t keyLength = key.length();
 690     // size needed to encode the key
 691     size_t size = keyLength + pbRawVarint32Size((int32_t) keyLength);
 692     // size needed to encode the value
 693     size += data.length() + pbRawVarint32Size((int32_t) data.length());
 694 
 695     SCOPEDLOCK(m_exclusiveProcessLock);//加写锁
 696 
 697     bool hasEnoughSize = ensureMemorySize(size);//检查是否有足够的空间
 698 
 699     if (!hasEnoughSize || !isFileValid()) {
 700         return false;
 701     }
 702     if (m_actualSize == 0) {
 703         auto allData = MiniPBCoder::encodeDataWithObject(m_dic);
 704         if (allData.length() > 0) {
 705             if (m_crypter) {
 706                 m_crypter->reset();
 707                 auto ptr = (unsigned char *) allData.getPtr();
 708                 m_crypter->encrypt(ptr, ptr, allData.length());
 709             }
 710             writeAcutalSize(allData.length());
 711             m_output->writeRawData(allData); // note: don't write size of data
 712             recaculateCRCDigest();
 713             return true;
 714         }
 715         return false;
 716     } else {
 717         writeAcutalSize(m_actualSize + size); //更新文件大小
 718         m_output->writeString(key);
 719         m_output->writeData(data); // note: write size of data
 720 
 721         auto ptr = (uint8_t *) m_ptr + Fixed32Size + m_actualSize - size;
 722         if (m_crypter) {
 723             m_crypter->encrypt(ptr, ptr, size);
 724         }
 725         updateCRCDigest(ptr, size, KeepSequence); //更新元数据的校验码
 726 
 727         return true;
 728     }
 729 }

 636 void MMKV::writeAcutalSize(size_t actualSize) {
 640     memcpy(m_ptr, &actualSize, Fixed32Size); //更新文件大小
 641     m_actualSize = actualSize;
 642 }

先检查是否有足够的空间ensureMemorySize

 560 bool MMKV::ensureMemorySize(size_t newSize) {
 561     if (!isFileValid()) {
 563         return false;
 564     }
 565 
 566     if (newSize >= m_output->spaceLeft()) { //内存不够
 568         static const int offset = pbFixed32Size(0);
 569         MMBuffer data = MiniPBCoder::encodeDataWithObject(m_dic); //重写数据,防止过多的修改和删除导致占用过多的内存空间,如redis的rewrite aof
 570         size_t lenNeeded = data.length() + offset + newSize;
 571         if (m_isAshmem) {
 572             if (lenNeeded > m_size) {//重写后还不够
 575                 return false;
 576             }
 577         } else {
 578             size_t futureUsage = newSize * std::max<size_t>(8, (m_dic.size() + 1) / 2);
 581             if (lenNeeded >= m_size || (lenNeeded + futureUsage) >= m_size) {
 582                 size_t oldSize = m_size;
 583                 do {
 584                     m_size *= 2;
 585                 } while (lenNeeded + futureUsage >= m_size); //内存空间增长策略
 589 
 590                 // if we can't extend size, rollback to old state
 591                 if (ftruncate(m_fd, m_size) != 0) {
 594                     m_size = oldSize;
 595                     return false;
 596                 } //调整文件大小成功
 597                 if (!zeroFillFile(m_fd, oldSize, m_size - oldSize)) {
 600                     m_size = oldSize;
 601                     return false;
 602                 } //初始化多出来的内存为0
 603 
 604                 if (munmap(m_ptr, oldSize) != 0) {
 606                 }//munmap的时候把共享内存中的数据写到文件,跟手动调用msync效果一样
 607                 m_ptr = (char *) mmap(m_ptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0); //重新mmap
 608                 if (m_ptr == MAP_FAILED) {
 610                 }
 611 
 612                 // check if we fail to make more space
 613                 if (!isFileValid()) {
 615                     return false;
 616                 }
 617             }
 618         }
 619 
 620         if (m_crypter) { //校验相关
 621             m_crypter->reset();
 622             auto ptr = (unsigned char *) data.getPtr();
 623             m_crypter->encrypt(ptr, ptr, data.length());
 624         }
 625 
 626         writeAcutalSize(data.length());
 627 
 628         delete m_output;
 629         m_output = new CodedOutputData(m_ptr + offset, m_size - offset);
 630         m_output->writeRawData(data); //写rewrite后的数据
 631         recaculateCRCDigest();
 632     }
 633     return true;
 634 }

删除:

1123 void MMKV::removeValueForKey(const std::string &key) {
1124     if (key.empty()) {
1125         return;
1126     }
1127     SCOPEDLOCK(m_lock);
1128     SCOPEDLOCK(m_exclusiveProcessLock); //写锁
1129     checkLoadData(); //数据是否是最新
1130 
1131     removeDataForKey(key);
1132 }

 674 bool MMKV::removeDataForKey(const std::string &key) {
 675     if (key.empty()) {
 676         return false;
 677     }
 678 
 679     auto deleteCount = m_dic.erase(key);
 680     if (deleteCount > 0) {
 681         static MMBuffer nan(0);  //删除标志
 682         return appendDataWithKey(nan, key);  ;//append(同写)
 683     }
 684 
 685     return false;
 686 }

持久化:

1154 void MMKV::sync() {
1155     SCOPEDLOCK(m_lock);
1156     if (m_needLoadFromFile || !isFileValid()) {
1157         return;
1158     }
1159     SCOPEDLOCK(m_exclusiveProcessLock);
1160     if (msync(m_ptr, m_size, MS_SYNC) != 0) {
1162     }
1163 }

这块一方面是由程序手动调用的,但是在MMKV::onExit的时候。

这里有几处设计思路可以学习下,一是如redis中的aof重写,一开始只管写,当文件达到一定大小后,把当前m_dic快照rewrite下,删除重复的对一个key的操作,以最后一个为准,相当于延迟计算。还有如leveldb中对key的删除,不真正删除,只设置一个特殊标志,当合并的时检查并作删除,这时rewrite的时候以m_dic为准,当然需要保证正常的key中不会含有特殊标志。

另外在recaculateCRCDigest中会对序列号自增IncreaseSequence=true,其他进程会重新mmap。在几处可能会发起这个调用:fullWriteback/loadFromFile/ensureMemorySize

 882 void MMKV::recaculateCRCDigest() {
 883     if (m_ptr && m_ptr != MAP_FAILED) {
 884         m_crcDigest = 0;
 885         constexpr int offset = pbFixed32Size(0);
 886         updateCRCDigest((const uint8_t *) m_ptr + offset, m_actualSize, IncreaseSequence);
 887     }
 888 }

其他的一些实现不多分析了,用到的时候再看看。这里删除和修改只是简单的在文件后面append,之后用后面的覆盖前面的,每次读写和其他接口,都要尝试是否重新load最新的数据。

总结下:这个开源项目涉及到的知识点有多进程通信共享内存,文件锁同步,递归锁,锁的升降级等和一些不错的设计实现。当然里面还有一些注意点,基础知识不扎实可能会出踩坑。
当然最重要的是根据相应的业务需求来选择适合的方案。

参考
msync
MMKV for Android 多进程设计与实现

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容

  • 介绍 MMKV is an efficient, small, easy-to-use mobile key-va...
    土土Edmond木阅读 962评论 0 1
  • 前言 好久没有更新常用的第三方库了。让我们来聊聊MMKV这个常用的第三方库。MMKV这个库是做什么的呢?他本质上的...
    yjy239阅读 10,558评论 7 13
  • 这部分主要是分析微信开源的mmkv框架,它的介绍在这里github[https://github.com/Tenc...
    fooboo阅读 2,488评论 0 7
  • MMKV 简介 MMKV——基于 mmap 的高性能通用 key-value 组件MMKV 是基于 mmap 内存...
    who_young阅读 1,770评论 0 2
  • 这篇是跟mmkv相关的分析,上篇主要是一些基本知识介绍。 五) 文件结构和文件锁 粗略介绍下文件结构,引用网上一张...
    fooboo阅读 714评论 0 1