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存儲方式
 
 ??眾所周知,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
實際使用的時候,protobuf最后其實會轉化成一長串的二進制,二進制的形式其實就可在任何平臺傳輸了,這里有個問題就是怎么一大串的二進制怎么隔開數據呢?
 做法就是每塊數據前加一個數據頭,表示數據類型及協議字段序號。
 msg1_head + msg1 + msg2_head + msg2 + …
 數據頭也是基于128bits的數值存儲方式,一般1個字節就可以表示:
? 簡而言之,protobuf有著可跨平臺的傳輸能力,快速轉化的效率
- 1、序列化和反序列化效率比 xml 和 json 都高
- 2、字段可以亂序,欠缺,因此可以用來兼容舊的協議,或者是減少協議數據。
簡單使用
相對于SP來說,mmkv的使用更為簡單,只不過這里的初識化流程需要我們手動添加到Application中(保證使用前調用即可)。
 下為Android java調用實例:
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_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 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 #elsereturn 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_mapMMKV *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(); # endifm_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_file:m_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_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 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::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_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; }參考內容
Protobuf協議格式詳解
Google 的開源技術protobuf 簡介與例子
詳解通信數據協議ProtoBuf
protobuf官方文檔
MMKVGit官方文檔
深度分析mmap:是什么 為什么 怎么用 性能總結
iOS進階——微信開源存儲框架MMKV(一)
Android 存儲優化 —— MMKV 集成與原理
總結
 
                            
                        - 上一篇: find与grep的区别
- 下一篇: 从没想过会有一个这样的机会|大疆招聘
