MMKV 是基於 mmap 內存映射的移動端通用 key-value 組件,底層序列化/反序列化使用 protobuf 實現,性能高,穩定性強。從 2015 年中至今在微信上使用,其性能和穩定性經過了時間的驗證。近期也已移植到 Android / macOS / Windows 平臺,一併開源。
MMKV 源起
在微信客戶端的日常運營中,時不時就會爆發特殊文字引起系統的 crash,參考文章,文章裏面設計的技術方案是在關鍵代碼前後進行計數器的加減,通過檢查計數器的異常,來發現引起閃退的異常文字。在會話列表、會話界面等有大量 cell 的地方,希望新加的計時器不會影響滑動性能;另外這些計數器還要永久存儲下來——因爲閃退隨時可能發生。這就需要一個性能非常高的通用 key-value 存儲組件,我們考察了 SharedPreferences、NSUserDefaults、SQLite 等常見組件,發現都沒能滿足如此苛刻的性能要求。考慮到這個防 crash 方案最主要的訴求還是實時寫入,而 mmap 內存映射文件剛好滿足這種需求,我們嘗試通過它來實現一套 key-value 組件
MMKV 原理
- 內存準備
通過 mmap 內存映射文件,提供一段可供隨時寫入的內存塊,App 只管往裏面寫數據,由操作系統負責將內存回寫到文件,不必擔心 crash 導致數據丟失。 - 數據組織
數據序列化方面我們選用 protobuf 協議,pb 在性能和空間佔用上都有不錯的表現。 - 寫入優化
考慮到主要使用場景是頻繁地進行寫入更新,我們需要有增量更新的能力。我們考慮將增量 kv 對象序列化後,append 到內存末尾。 - 空間增長
使用 append 實現增量更新帶來了一個新的問題,就是不斷 append 的話,文件大小會增長得不可控。我們需要在性能和空間上做個折中。
優點
-
多進程訪問
通過與 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 都有加密功能。
推薦使用 Maven:
dependencies {
implementation 'com.tencent:mmkv:1.0.23'
// replace "1.0.23" with any available version
}
private MMKV kv = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main)
String rootDir = MMKV.initialize(this);
kv = MMKV.defaultMMKV();
}
kv.encode("name","張三");
kv.encode("boolean",false);
kv.encode("int",132);
boolean bValue = kv.decodeBool("boolean");
Log.e("dobValue", "bValue bValue Bool " + bValue);
String name = kv.decodeString("name");
Log.e("dobValue", "bValue bValue name " + name);
int num = kv.decodeInt("int");
Log.e("dobValue", "bValue bValue num " + num);