心理健康教育网站建设,网站建设去哪,成都网站推广排名,杭州如何设计网站首页概述
MMKV 是基于 mmap 内存映射的移动端通用 key-value 组件#xff0c;底层序列化/反序列化使用 protobuf 实现#xff0c;性能高#xff0c;稳定性强。从 2015 年中至今#xff0c;在 iOS 微信上使用已有近 3 年#xff0c;其性能和稳定性经过了时间的验证。近期已移植…概述
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)但相比于JsonProtobuf有更高的转化效率时间效率和空间效率都是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 mmkvMMKV.defaultMMKV();//默认的mapid为mmkv.default
MMKV mmkv1MMKV.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_mapstring, MMKV *;//获取一个 unordered_mapg_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 mmkvMMKV.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
#elsereturn mmkvWithID(DEFAULT_MMAP_ID, DEFAULT_MMAP_SIZE, mode, cryptKey);
#endif
}
unordered_mapstd::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::endif (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();}
# elsem_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_CRYPTif (m_crypter) {if (m_metaInfo-m_version MMKVVersionRandomIV) {m_crypter-resetIV(m_metaInfo-m_vector, sizeof(m_metaInfo-m_vector));}}
#endifif (!m_file-isFileValid()) {m_file-reloadFromFile();//如果文件不是有效的文件大小等待则重新进行加载 m_filem_file(new MemoryFile(m_path))这里获取的对象}if (!m_file-isFileValid()) {MMKVError(file [%s] not valid, m_path.c_str());//重新加载后仍然无效则报错} else {// error checkingbool 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);}// 进行写入 Protobufif (needFullWriteback) {
#ifndef MMKV_DISABLE_CRYPTif (m_crypter) {MiniPBCoder::greedyDecodeMap(*m_dicCrypt, inputBuffer, m_crypter);} else
#endif{MiniPBCoder::greedyDecodeMap(*m_dic, inputBuffer); }} else {
#ifndef MMKV_DISABLE_CRYPTif (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 everythingSCOPED_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_CRYPTif (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_APPLEauto ret appendDataWithKey(data, key, itr-second, isDataHolder);
# else//存数据逻辑auto ret appendDataWithKey(data, key, isDataHolder);
# endifif (!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];
#endifreturn 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 keysize_t rawKeySize keyLength pbRawVarint32Size(keyLength);// ensureMemorySize() might change kvHolder.offset, so have to do it early{auto valueLength static_castuint32_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 spaceauto 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::maxsize_t(1, dicCount);size_t futureUsage avgItemSize * std::maxsize_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 cant 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_CRYPTif (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的函数才能继续往下执行。所以线程会被阻塞。
解决办法
实现高效的文件操作实现更精简的数据格式实现更优的数据更新方式解决多进程一致性线程阻塞问题