mmkv之基本介紹


 

1.MMKV 原理以及使用

MMKV是基於mmap內存映射的移動端通用key-value組件,底層序列化/反序列化使用protobuf實現,性能高,穩定性強。從2015年中至今,在iOS微信上使用已有近3年,近期移植到Android平台,移動端全平台通用,並全部在Github上開源。

MMKV 原理

內存准備:
  通過 mmap 內存映射文件,提供一段可供隨時寫入的內存塊,App 只管往里面寫數據,由操作系統負責將內存回寫到文件,不必擔心 crash 導致數據丟失。
數據組織:
  數據序列化方面我們選用 protobuf 協議,pb 在性能和空間占用上都有不錯的表現。考慮到我們要提供的是通用 kv 組件,key 可以限定是 string 字符串類型,value 則多種多樣(int/bool/double 等)。要做到通用的話,考慮將 value 通過 protobuf 協議序列化成統一的內存塊(buffer),然后就可以將這些 KV 對象序列化到內存中。
寫入優化: (重點關注這里!!!)
  標准 protobuf 不提供增量更新的能力,每次寫入都必須全量寫入。考慮到主要使用場景是頻繁地進行寫入更新,我們需要有增量更新的能力:將增量 kv 對象序列化后,直接 append 到內存末尾;這樣同一個 key 會有新舊若干份數據,最新的數據在最后;那么只需在程序啟動第一次打開 mmkv 時,不斷用后讀入的 value 替換之前的值,就可以保證數據是最新有效的。
空間增長: (還有這里!!!)
  使用 append 實現增量更新帶來了一個新的問題,就是不斷 append 的話,文件大小會增長得不可控。例如同一個 key 不斷更新的話,是可能耗盡幾百 M 甚至上 G 空間,而事實上整個 kv 文件就這一個 key,不到 1k 空間就存得下。這明顯是不可取的。我們需要在性能和空間上做個折中:以內存 pagesize 為單位申請空間,在空間用盡之前都是 append 模式;當 append 到文件末尾時,進行文件重整、key 排重,嘗試序列化保存排重結果;排重后空間還是不夠用的話,將文件擴大一倍,直到空間足夠。
數據有效性:
  考慮到文件系統、操作系統都有一定的不穩定性,我們另外增加了 crc 校驗,對無效數據進行甄別。在 iOS 微信現網環境上,我們觀察到有平均約 70萬日次的數據校驗不通過。

MMKV 原理

  • 內存准備
    通過 mmap 內存映射文件,提供一段可供隨時寫入的內存塊,App 只管往里面寫數據,由操作系統負責將內存回寫到文件,不必擔心 crash 導致數據丟失。

  • 數據組織
    數據序列化方面我們選用 protobuf 協議,pb 在性能和空間占用上都有不錯的表現。

  • 寫入優化
    考慮到主要使用場景是頻繁地進行寫入更新,我們需要有增量更新的能力。我們考慮將增量 kv 對象序列化后,append 到內存末尾。

  • 空間增長
    使用 append 實現增量更新帶來了一個新的問題,就是不斷 append 的話,文件大小會增長得不可控。我們需要在性能和空間上做個折中。

更詳細的設計原理參考前文 《MMKV——iOS 下基於 mmap 的高性能通用 key-value 組件》

MMKV for Android 特有功能

我們不是簡簡單單地照搬 iOS 的實現,在遷移到 Android 的過程中,深入分析了 Android 平台現有 kv 組件的痛點,在原有功能基礎上,開發了 Android 特有的功能。

    • 多進程訪問
      通過與 Android 開發同學的溝通,了解到系統自帶的 SharedPreferences 對多進程的支持不好。現有基於 ContentProvider 封裝的實現,雖然多進程是支持了,但是性能低下,經常導致 ANR。考慮到 mmap 共享內存本質上的多進程共享的,我們在這個基礎上,深入挖掘了 Android 系統的能力,提供了可能是業界最高效的多進程數據共享組件。具體實現原理我們中秋節后分享,心急的同學可以前往 GitHub 查看源碼和 wiki 文檔。

    • 匿名內存
      在多進程共享的基礎上,考慮到某些敏感數據(例如密碼)需要進程間共享,但是不方便落地存儲到文件上,直接用 mmap 不合適。我們了解到 Android 系統提供了 Ashmem 匿名共享內存的能力,發現它在進程退出后就會消失,不會落地到文件上,非常適合這個場景。我們很愉快地提供了 Ashmem MMKV 的功能。

    • 數據加密
      不像 iOS 提供了硬件層級的加密機制,在 Android 環境里,數據加密是非常必須的。MMKV 使用了 AES CFB-128 算法來加密/解密。我們選擇 CFB 而不是常見的 CBC 算法,主要是因為 MMKV 使用 append-only 實現插入/更新操作,流式加密算法更加合適。事實上這個功能也回饋到了 iOS 版,所以現在兩個系統的 MMKV 都有加密功能。

MMKV 是基於 mmap 內存映射的移動端通用 key-value 組件,底層序列化/反序列化使用 protobuf 實現,性能高,穩定性強。從 2015 年中至今,在 iOS 微信上使用已有近 3 年,其性能和穩定性經過了時間的驗證。近期已移植到 Android 平台。在騰訊內部開源半年之后,得到公司內部團隊的廣泛應用和一致好評。現在一並對外開源:https://github.com/tencent/mmkv

MMKV 源起

在微信客戶端的日常運營中,時不時就會爆發特殊文字引起系統的 crash,參考文章,文章里面設計的技術方案是在關鍵代碼前后進行計數器的加減,通過檢查計數器的異常,來發現引起閃退的異常文字。在會話列表、會話界面等有大量 cell 的地方,希望新加的計時器不會影響滑動性能;另外這些計數器還要永久存儲下來——因為閃退隨時可能發生。這就需要一個性能非常高的通用 key-value 存儲組件,我們考察了 SharedPreferences、NSUserDefaults、SQLite 等常見組件,發現都沒能滿足如此苛刻的性能要求。考慮到這個防 crash 方案最主要的訴求還是實時寫入,而 mmap 內存映射文件剛好滿足這種需求,我們嘗試通過它來實現一套 key-value 組件。

==============

 

MMKV 使用

implementation 'com.tencent:mmkv:1.0.19' 

在Application里面初始化:

protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); String rootDir = MMKV.initialize(this);//就這么一句話就行 System.out.println("mmkv root: " + rootDir); }

支持的數據類型:

支持以下 Java 語言基礎類型:
boolean、int、long、float、double、byte[],String、Set<String>,任何實現了Parcelable的類型,對象存儲方式是,轉化成json串,通過字符串存儲,

使用的時候在取出來反序列化.

增:

MMKV kv = MMKV.defaultMMKV(); kv.encode("bool", true); System.out.println("bool: " + kv.decodeBool("bool")); kv.encode("int", Integer.MIN_VALUE); System.out.println("int: " + kv.decodeInt("int")); kv.encode("long", Long.MAX_VALUE); System.out.println("long: " + kv.decodeLong("long")); kv.encode("float", -3.14f); System.out.println("float: " + kv.decodeFloat("float")); kv.encode("double", Double.MIN_VALUE); System.out.println("double: " + kv.decodeDouble("double")); kv.encode("string", "Hello from mmkv"); System.out.println("string: " + kv.decodeString("string")); byte[] bytes = {'m', 'm', 'k', 'v'}; kv.encode("bytes", bytes); System.out.println("bytes: " + new String(kv.decodeBytes("bytes"))); 

注意:mmkv的寫入邏輯是:當我們覆蓋某個值的時候,它並不會立即刪除前面的值,會保留,然后每個key,value有存儲限制,當觸發存儲限制的時候,才會執行刪除,這樣即使我們頻繁的覆蓋,也不會引起太多的性能損耗

刪:

MMKV kv = MMKV.defaultMMKV(); kv.removeValueForKey("bool"); System.out.println("bool: " + kv.decodeBool("bool")); kv.removeValuesForKeys(new String[]{"int", "long"}); System.out.println("allKeys: " + Arrays.toString(kv.allKeys())); 

改:

直接在存一遍就是.(執行增步驟)

查:

在增的步驟里面,已經打印可查的結果.

 kv.decodeBool("bool");
 kv.decodeInt("int");
.....

如果不同業務需要區別存儲,也可以單獨創建自己的實例:

MMKV* mmkv = MMKV.mmkvWithID("MyID");
mmkv.encode("bool", true);

SharedPreferences 遷移


    MMKV preferences = MMKV.mmkvWithID("myData"); // 遷移舊數據 { SharedPreferences old_man = getSharedPreferences("myData", MODE_PRIVATE); preferences.importFromSharedPreferences(old_man); old_man.edit().clear().commit(); } // 跟以前用法一樣 SharedPreferences.Editor editor = preferences.edit(); editor.putBoolean("bool", true); editor.putInt("int", Integer.MIN_VALUE); editor.putLong("long", Long.MAX_VALUE); editor.putFloat("float", -3.14f); editor.putString("string", "hello, imported"); HashSet<String> set = new HashSet<String>(); set.add("W"); set.add("e"); set.add("C"); set.add("h"); set.add("a"); set.add("t"); editor.putStringSet("string-set", set); // 無需調用 commit() //editor.commit(); 

以上內容來自官方github:https://github.com/Tencent/MMKV/wiki/android_setup_cn


 

MMKV 使用

Android 快速上手

MMKV 已托管到 bintray(JCenter),可以直接使用。在 App 的 build.gradle 里加上依賴:

MMKV 的使用非常簡單,所有變更立馬生效,無需調用 syncapply

在 App 啟動時初始化 MMKV,設定 MMKV 的根目錄(files/mmkv/),例如在 MainActivity 里:

MMKV 提供一個全局的實例,可以直接使用:

如果不同業務需要區別存儲,也可以單獨創建自己的實例:

SharedPreferences 遷移

  • MMKV 提供了 importFromSharedPreferences() 函數,可以比較方便地遷移數據過來。

  • MMKV 還額外實現了一遍 SharedPreferences、SharedPreferences.Editor 這兩個 interface,在遷移的時候只需兩三行代碼即可,其他 CRUD 操作代碼都不用改。

更詳細的用法可以參看 GitHub 上的 wiki 文檔。

MMKV 性能

Android 性能對比

我們將 MMKV 和 SharedPreferences、SQLite 進行對比, 重復讀寫操作 1k 次。相關測試代碼在 Android/MMKV/mmkvdemo/。結果如下圖表。

  • 單進程性能
    可見,MMKV 在寫入性能上遠遠超越 SharedPreferences & SQLite,在讀取性能上也有相近或超越的表現。

     
    image

    (測試機器是 Pixel 2 XL 64G,Android 8.1,每組操作重復 1k 次,時間單位是 ms。)

  • 多進程性能
    可見,MMKV 無論是在寫入性能還是在讀取性能,都遠遠超越 MultiProcessSharedPreferences & SQLite & SQLite, MMKV 在 Android 多進程 key-value 存儲組件上是不二之選

     
    image

    (測試機器是 Pixel 2 XL 64G,Android 8.1,每組操作重復 1k 次,時間單位是 ms。)

點擊原文直接訪問 GitHub 源碼。

初始化:

Application里面初始化:
 MMKV.initialize(this) 
//……一個全局的實例 // MMKV kv = MMKV.defaultMMKV(); MMKV mMkv = MMKV.mmkvWithID(Constant.MMKV_PREFERENCES, MMKV.SINGLE_PROCESS_MODE); // 存儲數據: mMkv.encode("Stingid", "123456"); mMkv.encode("bool", true); mMkv.encode("int", Integer.MIN_VALUE); // 取數據: String id=mMkv.decodeString("id", null); boolean bValue = mMkv.decodeBool("bool"); int iValue = kv.decodeInt("int"); // 移除數據: mMkv.remove("Stingid"); mMkv.remove("bool"); mMkv.remove("int");

 

MMKV淺析

MMKV 是微信開源的一個基於 mmap 內存映射的 key-value 組件,底層序列化/反序列化使用 protobuf 實現,性能高,穩定性強。微信團隊為了發現記錄特殊文字引起微信 iOS 系統的 crash,在關鍵代碼前后進行計數器的加減,通過檢查計數器的異常,來發現引起閃退的異常文字,但同時因為諸多cell的復雜頁面情境下希望新加的計時器不會影響性能,另外這些計數器需要永久存儲下來——因為閃退隨時可能發生,所以亟需高性能的通用 key-value 存儲組件,而微信團隊在實時寫入和高性能的選擇標准下,通過對比NSUserDefaults、SQLite 等常見組件,最終選擇了mmap 內存映射文件,並將其封裝成為了MMKV組件。下面我們來逐步進行了解

1.mmap簡介

認真分析mmap:是什么 為什么 怎么用這篇文章講的炒雞詳細,很佩服作者,本人也不想ctrl+c/v一遍, 。但在此處總結下:

mmap實現了一種使用內存映射到磁盤文件的方法,將本該屬於磁盤文件的對象映射到了進程地址空間中,實現文件磁盤地址和進程虛擬地址空間中一段虛擬地址的一一對映關系。實現這樣的映射關系后,進程就可以采用指針的方式讀寫操作這一段內存,而系統會自動(默認並不實時)回寫臟頁面到對應的文件磁盤上,即完成了對文件的操作而不必再調用read,write等系統調用函數,對文件直接通過內存映射讀取從而跨過了頁緩存,減少數據拷貝次數,用內存讀寫 取代I/O讀寫,提高文件讀取效率。另外,內核空間對這段區域的修改也直接反映用戶空間,從而可以實現不同進程間的文件共享,從而達到進程間通信和進程間共享的目的。簡言之,很強大。

2.protobuf

     ​ 在數據序列化方面微信團隊選用了protobuf 協議,出於通用化的考慮將多樣化的 value 通過 protobuf協議序列化成統一的內存塊(buffer),然后再進行相應存儲。

2.1 protobuf是什么

 

protobuf是一種靈活高效的序列化結構機制,就像xml,但是protobuf更輕量、更快並且更簡單。一旦你限定了你想要的數據結構,那么你就可以使用特殊的構建代碼實現對大量數據結構的讀寫,並且支持多種語言哦~你甚至可以更新你的數據構建,哪怕新的數據結構與老的完全相反,這絲毫不影響已經部署完成的程序。

也就是說protobuf幫我們輕松實現了序列化和反序列化,即使變更數據結構,也不會產生太大的影響,這對於數據結構多變的實際業務場景來說簡直太有必要了。

3.寫入優化&空間增長

​ 因為標准 protobuf並 不提供增量更新的能力,每次寫入都必須全量寫入。 查看代碼我們也能看到最底層調用的方法是使用的append而非直接替換:

- (BOOL)setRawData:(NSData *)data forKey:(NSString *)key {
    if (data.length <= 0 || key.length <= 0) {
        return NO;
    }
    CScopedLock lock(m_lock);

    [m_dic setObject:data forKey:key];
    m_hasFullWriteBack = NO;

    return [self appendData:data forKey:key];
}

但是這樣就會引發兩個問題:

1.很大程度上可能存在相同key但是存儲了多個不同的value。  2.不斷 append 的話,文件大小會增長得不可控。

針對這兩個問題的處理方式是:

1.在程序啟動第一次打開 mmkv 時,不斷用后讀入的 value 替換之前的值,就可以保證數據是最新有效的。

2.對於空間增長的問題:以內存 pagesize 為單位申請空間,在空間用盡之前都是 append 模式;當 append 到文件末尾時,進行文件重整、key 排重,嘗試序列化保存排重結果;排重后空間還是不夠用的話,將文件擴大一倍,直到空間足夠。所以在每次append之前都會先調用- (BOOL)ensureMemorySize:(size_t)newSize;方法檢查一下是否有足夠空間,如果沒有則按照每次2倍的大小去擴展空間:

- (BOOL)ensureMemorySize:(size_t)newSize { ... if (newSize >= m_output->spaceLeft()) { // try a full rewrite to make space static const int offset = pbFixed32Size(0); NSData *data = [MiniPBCoder encodeDataWithObject:m_dic]; size_t lenNeeded = data.length + offset + newSize; size_t avgItemSize = lenNeeded / std::max<size_t>(1, m_dic.count); size_t futureUsage = avgItemSize * std::max<size_t>(8, m_dic.count / 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 >= m_size || (lenNeeded + futureUsage) >= m_size) { size_t oldSize = m_size; do { m_size *= 2; } while (lenNeeded + futureUsage >= m_size); ... } ... } ... } 

3.另外針對空間增長,mmkv還提供了- (void)trim;方法來提供了通過手動調用減小多余占用內存的功能,正如每次擴增時按2倍擴增,縮減時也是每次除以2:

- (void)trim { ... auto oldSize = m_size; while (m_size > (m_actualSize * 2)) { m_size /= 2; } ... } 

4.crc 校驗

​ 微信團隊考慮到文件系統、操作系統都有一定的不穩定性,另外增加了 crc 校驗,對無效數據進行甄別,根據微信提供的數據:在 iOS 微信現網環境上,觀察到有平均約 70w 日次的數據校驗不通過。

4.1 crc 校驗簡介

​ CRC即循環冗余校驗碼(Cyclic Redundancy Check):是數據通信領域中最常用的一種查錯校驗碼,其特征是信息字段和校驗字段的長度可以任意選定。循環冗余檢查(CRC)是一種數據傳輸檢錯功能,對數據進行多項式計算,並將得到的結果附在幀的后面,接收設備也執行類似的算法,以保證數據傳輸的正確性和完整性。也就是接受方和發送方約定一個用來計算的二進制數(比如x),在整個傳輸過程中,這個數始終保持不變。循環冗余校驗碼(CRC)的基本原理是:在K位信息碼后再拼接R位的校驗碼,整個編碼長度為N位,因此,這種編碼也叫(N,K)碼。那么發送方發送時根據約定的x計算出要補全在K位信息碼后的R位校驗碼,然后發送,接收方接收到數據之后通過約定好的x對收到的數據進行校驗,即可查驗在數據傳輸過程中有否出錯。

 
對比結果

另外相比較NSUserDefaults還需要手動調用synchronize保存來說,MMKV為自動保存,無需手動調用同步。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM