我们还是从 testcase 出发,一点点看 MMKV 如何实现读写的
MMKV 写 1 2 3 val mmkv = MMKV.mmkvWithID("testKotlin" )mmkv.encode("bool" , true ) mmkv.encode("int" , Integer.MIN_VALUE)
MMKV#encode 支持所有基本类型, 这里我们挑一个简单的 bool 类型分析一下,根据调用链MMKV#encode
->MMKV#encodeBool
来到 native-bridge.cpp 中的 JNI 接口,找到其实现MMKV::set(bool value, MMKVKey_t key)
1 2 3 4 5 6 7 8 9 10 11 12 bool MMKV::set (bool value, MMKVKey_t key) { if (isKeyEmpty(key)) { return false ; } size_t size = pbBoolSize(); MMBuffer data (size) ; CodedOutputData output (data.getPtr(), size) ; output.writeBool(value); return setDataForKey(std ::move(data), key); }
这里根据 pb 协议的规范,分配了一块和 pb 协议中商定的类型字段长度相同的 buffer MMBuffer, 将这个 buffer 利用 CodedOutputData 这个工具类写入一个 bool,之后调用setDataForKey
来记录
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 bool MMKV::setDataForKey(MMBuffer &&data, MMKVKey_t key) { if (data.length() == 0 || isKeyEmpty(key)) { return false ; } SCOPED_LOCK(m_lock); SCOPED_LOCK(m_exclusiveProcessLock); checkLoadData(); auto ret = appendDataWithKey(data, key); if (ret) { m_dic[key] = std ::move(data); m_hasFullWriteback = false ; } return ret; }
checkLoadData() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 void MMKV::checkLoadData() { if (m_needLoadFromFile) { SCOPED_LOCK(m_sharedProcessLock); m_needLoadFromFile = false ; loadFromFile(); return ; } if (!m_isInterProcess) { return ; } if (!m_metaFile->isFileValid()) { return ; } MMKVMetaInfo metaInfo; metaInfo.read(m_metaFile->getMemory()); if (m_metaInfo->m_sequence != metaInfo.m_sequence) { MMKVInfo("[%s] oldSeq %u, newSeq %u" , m_mmapID.c_str(), m_metaInfo->m_sequence, metaInfo.m_sequence); SCOPED_LOCK(m_sharedProcessLock); clearMemoryCache(); loadFromFile(); notifyContentChanged(); } else if (m_metaInfo->m_crcDigest != metaInfo.m_crcDigest) { MMKVDebug("[%s] oldCrc %u, newCrc %u, new actualSize %u" , m_mmapID.c_str(), m_metaInfo->m_crcDigest, metaInfo.m_crcDigest, metaInfo.m_actualSize); SCOPED_LOCK(m_sharedProcessLock); size_t fileSize = m_file->getActualFileSize(); if (m_file->getFileSize() != fileSize) { MMKVInfo("file size has changed [%s] from %zu to %zu" , m_mmapID.c_str(), m_file->getFileSize(), fileSize); clearMemoryCache(); loadFromFile(); } else { partialLoadFromFile(); } notifyContentChanged(); } }
逻辑比较清晰,主要看看 partialLoadFromFile 这个函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 void MMKV::partialLoadFromFile() { m_metaInfo->read(m_metaFile->getMemory()); size_t oldActualSize = m_actualSize; m_actualSize = readActualSize(); auto fileSize = m_file->getFileSize(); MMKVDebug("loading [%s] with file size %zu, oldActualSize %zu, newActualSize %zu" , m_mmapID.c_str(), fileSize, oldActualSize, m_actualSize); if (m_actualSize > 0 ) { if (m_actualSize < fileSize && m_actualSize + Fixed32Size <= fileSize) { if (m_actualSize > oldActualSize) { size_t bufferSize = m_actualSize - oldActualSize; auto ptr = (uint8_t *) m_file->getMemory(); MMBuffer inputBuffer (ptr + Fixed32Size + oldActualSize, bufferSize, MMBufferNoCopy) ; m_crcDigest = (uint32_t ) CRC32(m_crcDigest, (const uint8_t *) inputBuffer.getPtr(), inputBuffer.length()); if (m_crcDigest == m_metaInfo->m_crcDigest) { if (m_crypter) { decryptBuffer(*m_crypter, inputBuffer); } MiniPBCoder::greedyDecodeMap(m_dic, inputBuffer, bufferSize); m_output->seek(bufferSize); m_hasFullWriteback = false ; MMKVDebug("partial loaded [%s] with %zu values" , m_mmapID.c_str(), m_dic.size()); return ; } else { MMKVError("m_crcDigest[%u] != m_metaInfo->m_crcDigest[%u]" , m_crcDigest, m_metaInfo->m_crcDigest); } } } } clearMemoryCache(); loadFromFile(); }
appendDataWithKey 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 bool MMKV::appendDataWithKey(const MMBuffer &data, MMKVKey_t key) { size_t keyLength = key.length(); size_t size = keyLength + pbRawVarint32Size((int32_t ) keyLength); size += data.length() + pbRawVarint32Size((int32_t ) data.length()); SCOPED_LOCK(m_exclusiveProcessLock); bool hasEnoughSize = ensureMemorySize(size); if (!hasEnoughSize || !isFileValid()) { return false ; } m_output->writeString(key); m_output->writeData(data); auto ptr = (uint8_t *) m_file->getMemory() + Fixed32Size + m_actualSize; if (m_crypter) { m_crypter->encrypt(ptr, ptr, size); } m_actualSize += size; updateCRCDigest(ptr, size); return true ; }
MMKV 读 同样地,通过 JNI 找到MMKV::getBool
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 bool MMKV::getBool(MMKVKey_t key, bool 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.readBool(); } catch (std ::exception &exception) { MMKVError("%s" , exception.what()); } } return defaultValue; }
实现关键在 getDataForKey
1 2 3 4 5 6 7 8 9 const MMBuffer &MMKV::getDataForKey(MMKVKey_t key) { checkLoadData(); auto itr = m_dic.find(key); if (itr != m_dic.end()) { return itr->second; } static MMBuffer nan; return nan; }
checkLoadData 函数通过调用 loadFromFile 会确保所有的文件数据被读进 m_dic 中,这里直接取就可以了
小结 MMKV 的读写逻辑比较简单,主要的实现还是依赖了 PB 方式的数据序列化和反序列化,根据代码逻辑,我们可以尝试还原出 MMKV 内部的存储结构如下:
buffer 1 2 3 4 5 6 7 8 9 message KV { string key = 1; buffer value = 2; } message MMKV { int32 size = 1; // 文件大小,用于校验新数据写入 repeated KV kv = 2; }
同时我们可以通过MiniPBCoder::decodeOneMap
的代码实现可以发现,MMKV 对于新旧数据问题,采用的方式不是写覆盖,而是直接追加,同时读时以最后一次写为最新值。这种方式显然是会带来大量的 key 字段冗余,因此必然存在一套完整的空间优化
MMKV 空间优化 核心实现在MMKV::ensureMemorySize
中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 bool MMKV::ensureMemorySize(size_t newSize) { if (!isFileValid()) { MMKVWarning("[%s] file not valid" , m_mmapID.c_str()); return false ; } constexpr size_t ItemSizeHolderSize = 4 ; if (m_dic.empty()) { newSize += ItemSizeHolderSize; } if (newSize >= m_output->spaceLeft() || m_dic.empty()) { auto fileSize = m_file->getFileSize(); MMBuffer data = MiniPBCoder::encodeDataWithObject(m_dic); size_t lenNeeded = data.length() + Fixed32Size + newSize; size_t avgItemSize = lenNeeded / std ::max<size_t >(1 , m_dic.size()); size_t futureUsage = avgItemSize * std ::max<size_t >(8 , (m_dic.size() + 1 ) / 2 ); 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 (!m_file->truncate(fileSize)) { return false ; } if (!isFileValid()) { MMKVWarning("[%s] file not valid" , m_mmapID.c_str()); return false ; } } return doFullWriteBack(std ::move(data)); } return true ; }
整个读写过程分析到这里,后面将分析一下 MMKV 对于跨进程存储的实现