MMKV 是基于 mmap 內(nèi)存映射的 key-value 組件,底層序列化 / 反序列化使用 protobuf 實(shí)現(xiàn),性能高配深,穩(wěn)定性強(qiáng)。從 2015 年中至今嫁盲,在 iOS 微信上使用已有近 3 年篓叶,其性能和穩(wěn)定性經(jīng)過了時(shí)間的驗(yàn)證。近期也已移植到 Android 平臺(tái)羞秤,一并開源缸托。
MMKV 源起
在微信客戶端的日常運(yùn)營中,時(shí)不時(shí)就會(huì)爆發(fā)特殊文字引起系統(tǒng)的 crash瘾蛋,參考文章俐镐,文章里面設(shè)計(jì)的技術(shù)方案是在關(guān)鍵代碼前后進(jìn)行計(jì)數(shù)器的加減,通過檢查計(jì)數(shù)器的異常哺哼,來發(fā)現(xiàn)引起閃退的異常文字佩抹。在會(huì)話列表、會(huì)話界面等有大量 cell 的地方取董,希望新加的計(jì)時(shí)器不會(huì)影響滑動(dòng)性能棍苹;另外這些計(jì)數(shù)器還要永久存儲(chǔ)下來 —— 因?yàn)殚W退隨時(shí)可能發(fā)生。這就需要一個(gè)性能非常高的通用 key-value 存儲(chǔ)組件茵汰,我們考察了 SharedPreferences枢里、NSUserDefaults、SQLite 等常見組件蹂午,發(fā)現(xiàn)都沒能滿足如此苛刻的性能要求栏豺。考慮到這個(gè)防 crash 方案最主要的訴求還是實(shí)時(shí)寫入豆胸,而 mmap 內(nèi)存映射文件剛好滿足這種需求奥洼,我們嘗試通過它來實(shí)現(xiàn)一套 key-value 組件。
MMKV 原理
- 內(nèi)存準(zhǔn)備
通過 mmap 內(nèi)存映射文件配乱,提供一段可供隨時(shí)寫入的內(nèi)存塊溉卓,App 只管往里面寫數(shù)據(jù)皮迟,由操作系統(tǒng)負(fù)責(zé)將內(nèi)存回寫到文件,不必?fù)?dān)心 crash 導(dǎo)致數(shù)據(jù)丟失桑寨。
數(shù)據(jù)組織
數(shù)據(jù)序列化方面我們選用 protobuf 協(xié)議伏尼,pb 在性能和空間占用上都有不錯(cuò)的表現(xiàn)。
寫入優(yōu)化
考慮到主要使用場(chǎng)景是頻繁地進(jìn)行寫入更新尉尾,我們需要有增量更新的能力爆阶。我們考慮將增量 kv 對(duì)象序列化后,append 到內(nèi)存末尾沙咏。空間增長(zhǎng)
使用 append 實(shí)現(xiàn)增量更新帶來了一個(gè)新的問題辨图,就是不斷 append 的話,文件大小會(huì)增長(zhǎng)得不可控肢藐。我們需要在性能和空間上做個(gè)折中故河。
Android 安裝指南(android_setup_cn · Tencent/MMKV Wiki · GitHub
)
基本要求
- MMKV 支持 API level 16 以上平臺(tái);
- MMKV 需使用 NDK r16b 或以上進(jìn)行編譯 (通過源碼引入 MMKV 的話)
安裝引入
從 v1.2.8 起, MMKV 遷移到 Maven Central吆豹。老版本 (<= v1.2.7) 仍然在 JCenter鱼的。
在 App 模塊的 build.gradle 文件里添加:
dependencies {
implementation 'com.tencent:mmkv:1.3.2'
// replace "1.3.2" with any available version
}
Gradle 在編譯工程的時(shí)候會(huì)自動(dòng)從 maven 倉庫下載 AAR 包。
MMKV 默認(rèn)以靜態(tài)庫形式鏈接 libc++痘煤。這個(gè)庫如果動(dòng)態(tài)鏈接凑阶,會(huì)額外占用 2MB 空間(解壓后)。如果你已經(jīng)有其他庫引入了 libc++_shared.so衷快,并且你確保他們的庫沒有版本兼容問題宙橱,你可以使用動(dòng)態(tài)鏈接 libc++ 的 MMKV,以進(jìn)一步減少安裝包大姓喊巍:
dependencies {
implementation 'com.tencent:mmkv-shared:1.3.2'
// replace "1.3.2" with any available version
}
性能對(duì)比
循環(huán)寫入隨機(jī)的 int 1k 次师郑,我們有如下性能對(duì)比:
MMKV 支持多進(jìn)程訪問。
快速上手
實(shí)現(xiàn)應(yīng)用的存儲(chǔ)功能通常在App啟動(dòng)時(shí)就要開始初始化其對(duì)象都伪,所以我們?cè)谧远x Application 內(nèi)對(duì) MMKV 初始化呕乎。
class GlobalApplication : Application() {
override fun onCreate() {
super.onCreate()
// 初始化
String rootDir = MMKV.initialize(this) // 返回mmkv的默認(rèn)存儲(chǔ)路徑(files/mmkv/)
}
}
MMKV 默認(rèn)把文件存放在$(FilesDir)/mmkv/目錄≡删В可以在 MMKV初始化時(shí)自定義根目錄
String dir = getFilesDir().getAbsolutePath() + "/mmkv";
String rootDir = MMKV.initialize(dir);
MMKV 創(chuàng)建實(shí)例:
// 獲取 MMKV 默認(rèn)全局實(shí)例,一般選用這個(gè)帝璧,本文也是選擇這個(gè)進(jìn)行實(shí)例創(chuàng)建
val mmkv = MMKV.defaultMMKV()
// 根據(jù)設(shè)置 id 來自定義 MMKV 對(duì)象先誉。比如根據(jù)業(yè)務(wù)來區(qū)分的存取實(shí)例
val mmkv = MMKV.mmkvWithID("ID")
// 開啟多進(jìn)程訪問。默認(rèn)是單線程
val mmkv = MMKV.mmkvWithID("ID",MMKV.MULTI_PROCESS_MODE)
寫數(shù)據(jù):
kv.encode("bool", true);
kv.encode("int", Integer.MIN_VALUE);
kv.encode("string", "Hello from mmkv");
讀數(shù)據(jù):
boolean bValue = kv.decodeBool("bool");
int iValue = kv.decodeInt("int");
String str = kv.decodeString("string");
刪除數(shù)據(jù):
刪除單個(gè)key:removeValueForKey(String key)
刪除多個(gè)key:removeValuesForKeys(String[] arrKeys)
清空所有數(shù)據(jù):clearAll()
從 SharedPreferences 遷移到 MMKV
為了演示怎么遷移的烁,我們重頭再來褐耳,在準(zhǔn)備部分的代碼基礎(chǔ)上臨時(shí)創(chuàng)建 SharedPreferences 儲(chǔ)存數(shù)據(jù)。
val sp = getSharedPreferences("test", Context.MODE_PRIVATE)
val edit = sp.edit()
// 為了驗(yàn)證順利遷移渴庆,我們初始值設(shè)置為10
edit.putInt("number",10);
edit.apply()
MMKV 提供了 importFromSharedPreferences 方法讓我們方便進(jìn)行 SP 數(shù)據(jù)的遷移铃芦,只要提供需要遷移的 SP 實(shí)例就能非逞拍鳎快速完成實(shí)例。
MMKV 實(shí)現(xiàn)了 SharedPreferences刃滓,Editor兩個(gè)接口仁烹,所以在遷移之后SP的操作代碼可以不用更改。
val oldData = getSharedPreferences("test",Context.MODE_PRIVATE)
mmkv.importFromSharedPreferences(oldData)
oldData.edit().clear().apply()
參考地址:MMKV首頁咧虎、文檔和下載 - 基于 mmap 的高性能通用 key-value 組件 - OSCHINA - 中文開源技術(shù)交流社區(qū)