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存储方式 ??众所周知,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 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;
}
如上创建了 Person 的结构并且把 name 设为 2,序列化好的二进制数据为:
0000 1000 0000 0010
? 简而言之,protobuf有着可跨平台的传输能力,快速转化的效率
- 1、序列化和反序列化效率比 xml 和 json 都高
- 2、字段可以乱序,欠缺,因此可以用来兼容旧的协议,或者是减少协议数据。
简单使用
相对于SP来说,mmkv的使用更为简单,只不过这里的初识化流程需要我们手动添加到Application中(保证使用前调用即可)。 下为Android java调用实例:
MMKV.initialize(this);
MMKV.initialize(this,"rootDir", MMKVLogLevel.LevelError);
MMKV mmkv=MMKV.defaultMMKV();
MMKV mmkv1=MMKV.mmkvWithID("1234");
mmkv.putInt("123",123);
mmkv.getInt("123",1235);
Android调用可直接依赖
dependencies {
implementation 'com.tencent:mmkv-static:1.2.10'
}
深入源码
因为MMKV的核心代码是由C语言编译的,对于Android端引来的Jar更多的是进行JNI的调用,所以在下面代码分析的时候更多的偏向于C的调用逻辑,至于Jar包中的调用流程不再放入。 大致剖析流程如下:
初识化 MMKV.initialize(this)
MMKV的初始化主要目的其实是对于mmkv的数据存储路径是否已经创建了,内部代码对多次初识化和多线程同时初识化进行了线程保护,这点可以学习。
void initialize() {
g_instanceDic = new unordered_map<string, MMKV *>;
g_instanceLock = new ThreadLock();
g_instanceLock->initialize();
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);
g_rootDir = rootDir;
mkPath(g_rootDir);
MMKVInfo("root dir: " MMKV_PATH_FORMAT, g_rootDir.c_str());
}
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);
}
获取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);
#else
return mmkvWithID(DEFAULT_MMAP_ID, DEFAULT_MMAP_SIZE, mode, cryptKey);
#endif
}
unordered_map<std::string, MMKV *> *g_instanceDic;
MMKV *MMKV::mmkvWithID(const string &mmapID, MMKVMode mode, string *cryptKey, MMKVPath_t *rootPath) {
if (mmapID.empty()) {
return nullptr;
}
SCOPED_LOCK(g_instanceLock);
auto mmapKey = mmapedKVKey(mmapID, rootPath);
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());
}
auto kv = new MMKV(mmapID, mode, cryptKey, rootPath);
kv->m_mmapKey = mmapKey;
(*g_instanceDic)[mmapKey] = kv;
return kv;
}
string mmapedKVKey(const string &mmapID, MMKVPath_t *rootPath) {
if (rootPath && g_rootDir != (*rootPath)) {
return md5(*rootPath + MMKV_PATH_SLASH + string2MMKVPath_t(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))
, 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
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;
m_hasFullWriteback = false;
m_crcDigest = 0;
m_lock->initialize();
m_sharedProcessLock->m_enable = m_isInterProcess;
m_exclusiveProcessLock->m_enable = m_isInterProcess;
{
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());
}
#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();
}
if (!m_file->isFileValid()) {
MMKVError("file [%s] not valid", m_path.c_str());
} else {
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();
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);
}
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 {
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字节,每次将数据写入前会先判断映射内存是否有足够的空间进行写入,如果空间不够就会进行扩容,每次扩容都是原理扩容的两倍,也就是前面提到的空间增长。动态的申请内存空间,用官方的话来说就是在性能和空间上做个折中。
bool MMKV::set(int32_t value, MMKVKey_t key) {
if (isKeyEmpty(key)) {
return false;
}
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);
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
{
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));
}
}
m_hasFullWriteback = false;
#ifdef MMKV_APPLE
[key retain];
#endif
return true;
}
KVHolderRet_t MMKV::appendDataWithKey(const MMBuffer &data, const KeyValueHolder &kvHolder, bool isDataHolder) {
SCOPED_LOCK(m_exclusiveProcessLock);
uint32_t keyLength = kvHolder.keySize;
size_t rawKeySize = keyLength + pbRawVarint32Size(keyLength);
{
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())) {
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);
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(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 集成与原理
|