MMKV
目標
了解MMKV
MMKV的基本應用
MMKV的原理概念
多進程設(shè)計思想
性能對比
源碼解讀
簡介
MMKV 是基于 mmap 內(nèi)存映射的 key-value 組件普气,底層序列化/反序列化使用 protobuf 實現(xiàn),性能高抹恳,穩(wěn)定性強锡搜。
官方文檔:https://github.com/Tencent/MMKV/blob/master/README_CN.md
項目地址:https://github.com/Tencent/MMKV
mmap
簡單解釋(僅供參考)
把文件描述符fd(部分硬件資源外存統(tǒng)一描述符)映射到虛擬空間中橙困,所以能夠?qū)崿F(xiàn)進程間的通信瞧掺、數(shù)據(jù)存取耕餐。
映射流程(僅供參考)
1、用戶進程調(diào)用內(nèi)存映射函數(shù)庫mmap辟狈,當前進程在虛擬地址空間中肠缔,尋找一段空閑的滿足要求的虛擬地址。
2哼转、此時內(nèi)核收到相關(guān)請求后會調(diào)用內(nèi)核的mmap函數(shù)明未,注意,不同于用戶空間庫函數(shù)壹蔓。內(nèi)核mmap函數(shù)通過虛擬文件系統(tǒng)定位到文件磁盤物理地址趟妥,既實現(xiàn)了文件地址和虛擬地址區(qū)域的映射關(guān)系。 此時佣蓉,這片虛擬地址并沒有任何數(shù)據(jù)關(guān)聯(lián)到主存中披摄。
注意亲雪,前兩個階段僅在于創(chuàng)建虛擬區(qū)間并完成地址映射,但是并沒有將任何文件數(shù)據(jù)的拷貝至主存疚膊。真正的文件讀取是當進程發(fā)起讀或?qū)懖僮鲿r义辕。
3、進程的讀或?qū)懖僮髟L問虛擬地址空間這一段映射地址寓盗,現(xiàn)這一段地址并不在物理頁面上灌砖。因為目前只建立了地址映射,真正的硬盤數(shù)據(jù)還沒有拷貝到內(nèi)存中傀蚌,因此引發(fā)缺頁中斷基显。
4、由于引發(fā)了缺頁中斷善炫,內(nèi)核則調(diào)用nopage函數(shù)把所缺的頁從磁盤裝入到主存中续镇。
5、之后用戶進程即可對這片主存進行讀或者寫的操作销部,如果寫操作改變了其內(nèi)容摸航,一定時間后系統(tǒng)會自動回寫臟頁面到對應磁盤地址,也即完成了寫入到文件的過程舅桩。
應用
Linux進程的創(chuàng)建
Android Binder
微信MMKV組件
美團Logan
參考文章
ProtoBuf
簡介
protocol buffers 是一種語言無關(guān)酱虎、平臺無關(guān)、可擴展的序列化結(jié)構(gòu)數(shù)據(jù)的方法擂涛,它可用于(數(shù)據(jù))通信協(xié)議读串、數(shù)據(jù)存儲等。
更多內(nèi)容撒妈、實際應用可參考官方文檔恢暖。
官方文檔:https://developers.google.com/protocol-buffers/docs/overview
特性
語言無關(guān)、平臺無關(guān):即 ProtoBuf 支持 Java狰右、C++杰捂、Python 等多種語言,支持多個平臺
高效:即比 XML 更衅灏觥(3 ~ 10倍)嫁佳、更快(20 ~ 100倍)、更為簡單
擴展性谷暮、兼容性好:你可以更新數(shù)據(jù)結(jié)構(gòu)蒿往,而不影響和破壞原有的舊程序
數(shù)據(jù)結(jié)構(gòu)
時間效率對比:
數(shù)據(jù)格式 | 1000條數(shù)據(jù) | 5000條數(shù)據(jù) |
---|---|---|
Protobuf | 195ms | 647ms |
Json | 515ms | 2293ms |
空間效率對比:
數(shù)據(jù)格式 | 5000條數(shù)據(jù) |
---|---|
Protobuf | 22MB |
Json | 29MB |
參考文章
http://www.reibang.com/p/73c9ed3a4877
http://www.reibang.com/p/a24c88c0526a
簡單使用
MMKV 的使用非常簡單,所有變更立馬生效湿弦,無需調(diào)用 sync
瓤漏、apply
。
依賴
dependencies {
implementation 'com.tencent:mmkv:1.0.10'
// replace "1.0.10" with any available version
}
初始化
配置 MMKV 根目錄
在 App 啟動時初始化 MMKV,設(shè)定 MMKV 的根目錄(files/mmkv/)蔬充,例如在 Application
里:
public void onCreate() {
super.onCreate();
String rootDir = MMKV.initialize(this);
System.out.println("mmkv root: " + rootDir);
//data/user/0包名/files/mmkv
}
其他初始化的方法
//指定日志級別
initialize(Context context, MMKVLogLevel logLevel)
//指定存儲地址和日志級別
initialize(String rootDir)
initialize(String rootDir, MMKVLogLevel logLevel)
//MMKV.LibLoader用來解決Android 設(shè)備(API level 19)在安裝/更新 APK 時出錯問題
initialize(String rootDir, MMKV.LibLoader loader)
initialize(String rootDir, MMKV.LibLoader loader, MMKVLogLevel logLevel)
CRUD 操作
MMKV 提供一個全局的實例俯在,可以直接使用:
import com.tencent.mmkv.MMKV;
//……
MMKV kv = MMKV.defaultMMKV();
kv.encode("bool", true);
boolean bValue = kv.decodeBool("bool");
kv.encode("int", Integer.MIN_VALUE);
int iValue = kv.decodeInt("int");
kv.encode("string", "Hello from mmkv");
String str = kv.decodeString("string");
刪除 & 查詢
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()));
boolean hasBool = kv.containsKey("bool");
區(qū)分存儲
使用MMKV.mmkvWithID即可創(chuàng)建不同的存儲區(qū)域的MMKV實例。
MMKV kv = MMKV.mmkvWithID("MyID");
kv.encode("bool", true);
支持的數(shù)據(jù)類型
-
支持以下 Java 語言基礎(chǔ)類型:
boolean娃惯、int跷乐、long、float趾浅、double愕提、byte[]
-
支持以下 Java 類和容器:
String、Set<String>
- 任何實現(xiàn)了
Parcelable
的類型
SharedPreferences 遷移
- MMKV 提供了
importFromSharedPreferences()
函數(shù)皿哨,可以比較方便地遷移數(shù)據(jù)過來
/**
* An highly efficient, reliable, multi-process key-value storage framework.
* THE PERFECT drop-in replacement for SharedPreferences and MultiProcessSharedPreferences.
*/
public class MMKV implements SharedPreferences, SharedPreferences.Editor {
- MKV 還額外實現(xiàn)了一遍
SharedPreferences
浅侨、SharedPreferences.Editor
這兩個 interface,在遷移的時候只需兩三行代碼即可证膨,其他 CRUD 操作代碼都不用改如输。
private void testImportSharedPreferences() {
//SharedPreferences preferences = getSharedPreferences("myData", MODE_PRIVATE);
MMKV preferences = MMKV.mmkvWithID("myData");
// 遷移舊數(shù)據(jù)
{
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);
// 無需調(diào)用 commit()
//editor.commit();
}
進階使用
日志
日志切面AOP思想
MMKV 默認將日志打印到 logcat,不便于對線上問題進行定位和解決央勒。你可以在 App 啟動時接收轉(zhuǎn)發(fā) MMKV 的日志不见。實現(xiàn)MMKVHandler
接口,添加類似下面的代碼:
@Override
public boolean wantLogRedirecting() {
return true;
}
@Override
public void mmkvLog(MMKVLogLevel level, String file, int line, String func, String message) {
String log = "<" + file + ":" + line + "::" + func + "> " + message;
switch (level) {
case LevelDebug:
//Log.d("redirect logging MMKV", log);
break;
case LevelInfo:
//Log.i("redirect logging MMKV", log);
break;
case LevelWarning:
//Log.w("redirect logging MMKV", log);
break;
case LevelError:
//Log.e("redirect logging MMKV", log);
break;
case LevelNone:
//Log.e("redirect logging MMKV", log);
break;
}
}
如果你不希望 MMKV 打印日志崔步,你可以關(guān)掉它(雖然我們強烈不建議你這么做)稳吮。
注意:除非有非常強烈的證據(jù)表明MMKV的日志拖慢了App的速度,你不應該關(guān)掉日志井濒。沒有日志灶似,日后萬一用戶有問題,將無法跟進瑞你。
MMKV.setLogLevel(MMKVLogLevel.LevelNone);
加密
MMKV 默認明文存儲所有 key-value酪惭,依賴 Android 系統(tǒng)的沙盒機制保證文件加密。如果你擔心信息泄露者甲,你可以選擇加密 MMKV春感。
String cryptKey = "My-Encrypt-Key";
MMKV kv = MMKV.mmkvWithID("MyID", MMKV.SINGLE_PROCESS_MODE, cryptKey);
你可以更改密鑰,也可以將一個加密 MMKV 改成明文过牙,或者反過來甥厦。
final String mmapID = "testAES_reKey1";
// an unencrypted MMKV instance
MMKV kv = MMKV.mmkvWithID(mmapID, MMKV.SINGLE_PROCESS_MODE, null);
// change from unencrypted to encrypted
kv.reKey("Key_seq_1");
// change encryption key
kv.reKey("Key_seq_2");
// change from encrypted to unencrypted
kv.reKey(null);
自定義 library loader
一些 Android 設(shè)備(API level 19)在安裝/更新 APK 時可能出錯, 導致 libmmkv.so 找不到。然后就會遇到 java.lang.UnsatisfiedLinkError
之類的 crash寇钉。有個開源庫 ReLinker 專門解決這個問題,你可以用它來加載 MMKV:
String dir = getFilesDir().getAbsolutePath() + "/mmkv";
MMKV.initialize(dir, new MMKV.LibLoader() {
@Override
public void loadLibrary(String libName) {
ReLinker.loadLibrary(MyApplication.this, libName);
}
});
Relinker簡介:
本地庫加載框架舶赔,github1000+的star
原理:
嘗試使用系統(tǒng)原生方式去加載so扫倡,如果加載失敗,Relinker會嘗試從apk中拷貝so到App沙箱目錄下,然后再去嘗試加載so撵溃。最終疚鲤,我們可以使用 ReLinker.loadLibrary(context, “mylibrary”) 來加載本地庫。
Native Buffer
當從 MMKV 取一個 String
or byte[]
的時候缘挑,會有一次從 native 到 JVM 的內(nèi)存拷貝集歇。如果這個值立即傳遞到另一個 native 庫(JNI),又會有一次從 JVM 到 native 的內(nèi)存拷貝语淘。當這個值比較大的時候诲宇,整個過程會非常浪費。Native Buffer 就是為了解決這個問題惶翻。
Native Buffer 是由 native 創(chuàng)建的內(nèi)存緩沖區(qū)姑蓝,在 Java 里封裝成 NativeBuffer
類型,可以透明傳遞到另一個 native 庫進行訪問處理吕粗。整個過程避免了先拷內(nèi)存到 JVM 又從 JVM 拷回來導致的浪費纺荧。示例代碼:
int sizeNeeded = kv.getValueActualSize("bytes");
NativeBuffer nativeBuffer = MMKV.createNativeBuffer(sizeNeeded);
if (nativeBuffer != null) {
int size = kv.writeValueToNativeBuffer("bytes", nativeBuffer);
Log.i("MMKV", "size Needed = " + sizeNeeded + " written size = " + size);
// pass nativeBuffer to another native library
// ...
// destroy when you're done
MMKV.destroyNativeBuffer(nativeBuffer);
}
跨進程通信的實現(xiàn)
本質(zhì):共享MMKV實例化信息完成對象的偽復制
-
通信的數(shù)據(jù)對象
該類MMKV內(nèi)部已經(jīng)實現(xiàn),傳遞進程A的mmkv信息給B進程颅筋,B進程新建MMKV實例宙暇,B就可以通過MMKV實例來完成數(shù)據(jù)的操作
public final class ParcelableMMKV implements Parcelable {
private final String mmapID;
private int ashmemFD = -1;
private int ashmemMetaFD = -1;
private String cryptKey = null;
public ParcelableMMKV(MMKV mmkv) {
mmapID = mmkv.mmapID();
ashmemFD = mmkv.ashmemFD();
ashmemMetaFD = mmkv.ashmemMetaFD();
cryptKey = mmkv.cryptKey();
}
private ParcelableMMKV(String id, int fd, int metaFD, String key) {
mmapID = id;
ashmemFD = fd;
ashmemMetaFD = metaFD;
cryptKey = key;
}
public MMKV toMMKV() {
if (ashmemFD >= 0 && ashmemMetaFD >= 0) {
return MMKV.mmkvWithAshmemFD(mmapID, ashmemFD, ashmemMetaFD, cryptKey);
}
return null;
}
}
-
Aidl文件,需要手動創(chuàng)建該文件
import com.tencent.mmkv.ParcelableMMKV; interface IAshmemMMKV { ParcelableMMKV GetAshmemMMKV(); }
Aidl定義了跨進程通信的方法細則议泵,這里只需要一個get方法客给,返回ParcelableMMKV通信實體。
-
服務(wù)端
服務(wù)端Service
public class UserServer extends Service { @Nullable @Override public IBinder onBind(Intent intent) { Log.i(TAG, "onBind, intent=" + intent); return new AshmemMMKVGetter(); } } public class AshmemMMKVGetter extends IAshmemMMKV.Stub { private AshmemMMKVGetter() { // 1M, ashmem cannot change size after opened final String id = "tetAshmemMMKV"; try { m_ashmemMMKV = MMKV.mmkvWithAshmemID(BenchMarkBaseService.this, id, AshmemMMKV_Size, MMKV.MULTI_PROCESS_MODE, CryptKey); m_ashmemMMKV.encode("bool", true); } catch (Exception e) { Log.e("MMKV", e.getMessage()); } } public ParcelableMMKV GetAshmemMMKV() { return new ParcelableMMKV(m_ashmemMMKV); } }
-
客戶端
onServiceConnected連接之后
Intent intent = new Intent();
intent.setAction("***.***.***");
intent.setPackage("***.***.***");
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
private ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
IAshmemMMKV ashmemMMKV = IAshmemMMKV.Stub.asInterface(service);
try {
ParcelableMMKV parcelableMMKV = ashmemMMKV.GetAshmemMMKV();
if (parcelableMMKV != null) {
m_ashmemMMKV = parcelableMMKV.toMMKV();
if (m_ashmemMMKV != null) {
Log.i("MMKV", "ashmem bool: " + m_ashmemMMKV.decodeBool("bool"));
}
}
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
isBind = false;
}
};
原理
內(nèi)存準備
通過 mmap 內(nèi)存映射文件肢簿,提供一段可供隨時寫入的內(nèi)存塊靶剑,App 只管往里面寫數(shù)據(jù),由操作系統(tǒng)負責將內(nèi)存回寫到文件池充,不必擔心 crash 導致數(shù)據(jù)丟失桩引。
數(shù)據(jù)組織
數(shù)據(jù)序列化方面我們選用 protobuf 協(xié)議,pb 在性能和空間占用上都有不錯的表現(xiàn)收夸】咏常考慮到我們要提供的是通用 kv 組件,key 可以限定是 string 字符串類型卧惜,value 則多種多樣(int/bool/double 等)厘灼。要做到通用的話,考慮將 value 通過 protobuf 協(xié)議序列化成統(tǒng)一的內(nèi)存塊(buffer)咽瓷,然后就可以將這些 KV 對象序列化到內(nèi)存中设凹。
message KV {
string key = 1;
buffer value = 2;
}
-(BOOL)setInt32:(int32_t)value forKey:(NSString*)key {
auto data = PBEncode(value);
return [self setData:data forKey:key];
}
-(BOOL)setData:(NSData*)data forKey:(NSString*)key {
auto kv = KV { key, data };
auto buf = PBEncode(kv);
return [self write:buf];
}
寫入優(yōu)化
標準 protobuf 不提供增量更新的能力,每次寫入都必須全量寫入茅姜∩林欤考慮到主要使用場景是頻繁地進行寫入更新,我們需要有增量更新的能力:將增量 kv 對象序列化后,直接 append 到內(nèi)存末尾奋姿;這樣同一個 key 會有新舊若干份數(shù)據(jù)锄开,最新的數(shù)據(jù)在最后;那么只需在程序啟動第一次打開 mmkv 時称诗,不斷用后讀入的 value 替換之前的值萍悴,就可以保證數(shù)據(jù)是最新有效的。
空間增長
使用 append 實現(xiàn)增量更新帶來了一個新的問題寓免,就是不斷 append 的話癣诱,文件大小會增長得不可控。例如同一個 key 不斷更新的話再榄,是可能耗盡幾百 M 甚至上 G 空間狡刘,而事實上整個 kv 文件就這一個 key,不到 1k 空間就存得下困鸥。這明顯是不可取的嗅蔬。我們需要在性能和空間上做個折中:以內(nèi)存 pagesize 為單位申請空間,在空間用盡之前都是 append 模式疾就;當 append 到文件末尾時澜术,進行文件重整、key 排重猬腰,嘗試序列化保存排重結(jié)果鸟废;排重后空間還是不夠用的話,將文件擴大一倍姑荷,直到空間足夠盒延。
-(BOOL)append:(NSData*)data {
if (space >= data.length) {
append(fd, data);
} else {
newData = unique(m_allKV);
if (total_space >= newData.length) {
write(fd, newData);
} else {
while (total_space < newData.length) {
total_space *= 2;
}
ftruncate(fd, total_space);
write(fd, newData);
}
}
}
數(shù)據(jù)有效性
考慮到文件系統(tǒng)、操作系統(tǒng)都有一定的不穩(wěn)定性鼠冕,我們另外增加了 crc 校驗添寺,對無效數(shù)據(jù)進行甄別。在 iOS 微信現(xiàn)網(wǎng)環(huán)境上懈费,我們觀察到有平均約 70萬日次的數(shù)據(jù)校驗不通過计露。
多進程設(shè)計思想
官網(wǎng)地址:https://github.com/Tencent/MMKV/wiki/android_ipc
官網(wǎng)有詳細的說明,這里主要分享思想:
CS架構(gòu):
IPC CS架構(gòu)有Binder憎乙、Socket等票罐,特點是一個單獨進程管理數(shù)據(jù),數(shù)據(jù)同步不易出錯泞边,簡單好用易上手该押,缺點是慢。
去中心化:
只需要將文件 mmap 到每個訪問進程的內(nèi)存空間繁堡,加上合適的進程鎖沈善,再處理好數(shù)據(jù)的同步乡数,就能夠?qū)崿F(xiàn)多進程并發(fā)訪問椭蹄。
性能對比
單進程
讀寫效率
mmkv | SharedPreferences | sqlite | |
---|---|---|---|
write int 1000 | 6.5 | 693.1 | 774.4 |
write String 1000 | 18.9 | 1003.9 | 857.3 |
read int 1000 | 4.3 | 1.5 | 302.9 |
read String 1000 | 8.3 | 1.3 | 320.7 |
單進程性能
可見闻牡,MMKV 在寫入性能上遠遠超越 SharedPreferences & SQLite,在讀取性能上也有相近或超越的表現(xiàn)绳矩。
(測試機器是 華為 Mate 20 Pro 128G罩润,Android 10,每組操作重復 1k 次翼馆,時間單位是 ms割以。)
多進程性能
可見,MMKV 無論是在寫入性能還是在讀取性能应媚,都遠遠超越 MultiProcessSharedPreferences & SQLite & SQLite严沥。
性能對比: https://github.com/Tencent/MMKV/wiki/android_benchmark_cn
原理上和SharedPreference區(qū)別
SharedPreference原理
本質(zhì)是在本地磁盤記錄了一個xml文件,在構(gòu)造方法中開啟一個子線程加載磁盤中的xml文件
@UnsupportedAppUsage
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
SharedPreferencesImpl內(nèi)部維護Map緩存中姜,所以SharedPreference讀的效率很高消玄,但是寫得時候都是通過FileOutputStreame文件IO得方式完成數(shù)據(jù)更新操作。
MMKV
利用mmap完成數(shù)據(jù)的讀寫丢胚,讀寫高效翩瓜。
SharedPreference | MMKV | |
---|---|---|
讀寫方式 | IO | mmap |
數(shù)據(jù)格式 | XML | 總體結(jié)構(gòu)、整型編碼携龟、二進制 |
更新方式 | 全量更新 | 增量與全量寫入 |
SharedPreferences注意點
- 只要file name相同睦柴,拿到的就是同一個SharedPreferencesImpl對象围肥,內(nèi)部有緩存機制,首次獲取才會創(chuàng)建對象。
- 在SharedPreferencesImpl構(gòu)造方法中躬络,會開啟子線程把對應的文件key-value全部加載進內(nèi)存,加載結(jié)束后盯捌,mLoaded被設(shè)置為true拔莱。
- 調(diào)用getXXX方法時,會阻塞等待直到mLoaded為true匿又,也就是getXXX方法是有可能阻塞UI線程的方灾,另外,調(diào)用contains和 edit等方法也是碌更。
- 寫數(shù)據(jù)時裕偿,會先拿到一個EditorImpl對象,然后putXXX痛单,這時只是把數(shù)據(jù)寫入到內(nèi)存中嘿棘,最后調(diào)用commit或者apply方法,才會真正寫入文件旭绒。
- 不管是commit還是apply方法鸟妙,第一步都是調(diào)用commitToMemory方法生成一個MemoryCommitResult對象焦人,注意這里會先處理clear舊的key-value,再處理新添加的key-value重父,另外value為this或者null都表示需要被remove掉花椭。
- 調(diào)用commit方法,就會同步執(zhí)行寫入文件的操作房午,該方法是耗時操作矿辽,不能在主線程中調(diào)用,該方法最后會返回成功或失敗結(jié)果郭厌。
- 調(diào)用apply方法袋倔,就會把任務(wù)放到QueuedWork的隊列中,然后在HandlerThread中執(zhí)行折柠,然后apply方法會立即返回宾娜。但如果是Android8.0之前,這里就是放到QueuedWork的一個單線程中執(zhí)行了扇售。
- 最后是寫入文件前塔,會先把原有的文件命名為bak備份文件,然后創(chuàng)建新的文件全量寫入缘眶,寫入成功后嘱根,把bak備份文件刪除掉。
安全
基于Android的沙盒模式巷懈,在內(nèi)存讀寫的方式上做了改變该抒,所以不存在應用程序之前的安全問題。
MMKV使用ProtoBuf 編碼顶燕,另外增加了內(nèi)部實現(xiàn)的加密模式(AES CFB)凑保,相比SharedPrefrence,在文件暴露的情況下MMKV的數(shù)據(jù)不具有可讀性涌攻。
在TV中的應用
配置參數(shù)較多欧引、需要頻繁讀寫修改參數(shù)的場景
可以提高讀寫耗時,減少SP帶來的耗時成本和操作不當引發(fā)的ANR
源碼解讀
初始化
public static String initialize(Context context) {
String root = context.getFilesDir().getAbsolutePath() + "/mmkv";
MMKVLogLevel logLevel = MMKVLogLevel.LevelInfo;
return initialize(root, (MMKV.LibLoader)null, logLevel);
}
public static String initialize(String rootDir, LibLoader loader, MMKVLogLevel logLevel) {
if (loader != null) {
if (BuildConfig.FLAVOR.equals("SharedCpp")) {
loader.loadLibrary("c++_shared");
}
loader.loadLibrary("mmkv");
} else {
if (BuildConfig.FLAVOR.equals("SharedCpp")) {
System.loadLibrary("c++_shared");
}
System.loadLibrary("mmkv");
}
MMKV.rootDir = rootDir;
jniInitialize(MMKV.rootDir, logLevel2Int(logLevel));
return rootDir;
}
1.當不指定目錄的時候恳谎,會創(chuàng)建一個app內(nèi)的/data/data/包名/files/mmkv的目錄芝此。所有的文件都保存在里面;
2.加載兩個so庫因痛,c++_shared以及mmkv婚苹, 根據(jù)打包配置來選擇是否要加載c++_shared
native_bridge.cpp
MMKV_JNI void jniInitialize(JNIEnv *env, jobject obj, jstring rootDir, jint logLevel) {
if (!rootDir) {
return;
}
const char *kstr = env->GetStringUTFChars(rootDir, nullptr);
if (kstr) {
//獲取rootDir的url char指針數(shù)組字符串,調(diào)用MMKV::initializeMMKV進一步初始化鸵膏。
MMKV::initializeMMKV(kstr, (MMKVLogLevel) logLevel);
env->ReleaseStringUTFChars(rootDir, kstr);
}
}
MMKV.cpp
void initialize() {
//創(chuàng)建了MMKV實例的散列表
g_instanceDic = new unordered_map<string, MMKV *>;
g_instanceLock = new ThreadLock();
g_instanceLock->initialize();
mmkv::DEFAULT_MMAP_SIZE = mmkv::getPageSize();
MMKVInfo("page size:%d", DEFAULT_MMAP_SIZE);
}
ThreadOnceToken_t once_control = ThreadOnceUninitialized;
void MMKV::initializeMMKV(const MMKVPath_t &rootDir, MMKVLogLevel logLevel) {
g_currentLogLevel = logLevel;
//初始化全局的線程鎖ThreadLock
ThreadLock::ThreadOnce(&once_control, initialize);
g_rootDir = rootDir;
//創(chuàng)建文件夾
mkPath(g_rootDir);
MMKVInfo("root dir: " MMKV_PATH_FORMAT, g_rootDir.c_str());
}
MMKV 的實例化
java層的實例化
defaultMMKV
public static MMKV defaultMMKV() {
if (rootDir == null) {
throw new IllegalStateException("You should Call MMKV.initialize() first.");
}
long handle = getDefaultMMKV(SINGLE_PROCESS_MODE, null);
return new MMKV(handle);
}
//構(gòu)造函數(shù)
private MMKV(long handle) {
nativeHandle = handle;
}
getDefaultMMKV Native層做好實例化工作返回一個long類型的handle膊升,以這個handler作為Java層MMKV的構(gòu)造參數(shù)
mmkvWithID
與defaultMMKV區(qū)別就是多了參數(shù)設(shè)置
public static MMKV mmkvWithID(String mmapID) {
if (rootDir == null) {
throw new IllegalStateException("You should Call MMKV.initialize() first.");
}
long handle = getMMKVWithID(mmapID, SINGLE_PROCESS_MODE, null, null);
return new MMKV(handle);
}
native層實例化
native-bridge.cpp==>getDefaultMMKV
MMKV.cpp==>mmkvWithID 默認的ID為mmkv.default
native-bridge.cpp
MMKV_JNI jlong getDefaultMMKV(JNIEnv *env, jobject obj, jint mode, jstring cryptKey) {
MMKV *kv = nullptr;
if (cryptKey) {
string crypt = jstring2string(env, cryptKey);
if (crypt.length() > 0) {
kv = MMKV::defaultMMKV((MMKVMode) mode, &crypt);
}
}
if (!kv) {
kv = MMKV::defaultMMKV((MMKVMode) mode, nullptr);
}
return (jlong) kv;
}
MMKV.cpp
#define DEFAULT_MMAP_ID "mmkv.default"
MMKV *MMKV::defaultMMKV(MMKVMode mode, string *cryptKey) {
#ifndef MMKV_ANDROID
return mmkvWithID(DEFAULT_MMAP_ID, mode, cryptKey);
#else
return mmkvWithID(DEFAULT_MMAP_ID, DEFAULT_MMAP_SIZE, mode, cryptKey);
#endif
}
MMKV.h
static MMKV *mmkvWithID(const std::string &mmapID,
int size = mmkv::DEFAULT_MMAP_SIZE,
MMKVMode mode = MMKV_SINGLE_PROCESS,
std::string *cryptKey = nullptr,
MMKVPath_t *relativePath = nullptr);
只要是實例化,最后都是調(diào)用mmkvWithID進行實例化谭企。默認的mmkv的id就是mmkv.default
mmkvWithID
MMKV.cpp
MMKV *MMKV::mmkvWithID(const string &mmapID, int size, MMKVMode mode, string *cryptKey, string *relativePath) {
if (mmapID.empty()) {
return nullptr;
}
SCOPED_LOCK(g_instanceLock);
//取 mmapID relativePath MMKV_PATH_SLASH 的 md5值作為key
auto mmapKey = mmapedKVKey(mmapID, relativePath);
auto itr = g_instanceDic->find(mmapKey);
if (itr != g_instanceDic->end()) {
MMKV *kv = itr->second;
return kv;
}
if (relativePath) {
if (!isFileExist(*relativePath)) {
if (!mkPath(*relativePath)) {
return nullptr;
}
}
MMKVInfo("prepare to load %s (id %s) from relativePath %s", mmapID.c_str(), mmapKey.c_str(),
relativePath->c_str());
}
//實例化
auto kv = new MMKV(mmapID, size, mode, cryptKey, relativePath);
(*g_instanceDic)[mmapKey] = kv;
return kv;
}
將所有的MMKV實例都會保存在之前實例化的g_instanceDic散列表中廓译。其中mmkv每一個id對應一個文件的路徑:
- 相對路徑(android中是 data/data/包名/files/mmkv) + / + mmkvID
如果發(fā)現(xiàn)對應路徑下的mmkv在散列表中已經(jīng)緩存了评肆,則直接返回。否則就會把相對路徑保存下來非区,傳遞給MMKV進行實例化瓜挽,并保存在g_instanceDic散列表中。
MMKV 的構(gòu)造函數(shù)
MMKV::MMKV(const string &mmapID, int size, MMKVMode mode, string *cryptKey, string *relativePath)
: m_mmapID(mmapedKVKey(mmapID, relativePath)) // historically Android mistakenly use mmapKey as mmapID
, m_path(mappedKVPathWithID(m_mmapID, mode, relativePath))
, m_crcPath(crcPathWithID(m_mmapID, mode, relativePath))
, m_file(new MemoryFile(m_path, size, (mode & MMKV_ASHMEM) ? MMFILE_TYPE_ASHMEM : MMFILE_TYPE_FILE))
, m_metaFile(new MemoryFile(m_crcPath, DEFAULT_MMAP_SIZE, m_file->m_fileType))
, m_metaInfo(new MMKVMetaInfo())
, m_crypter(nullptr)
, m_lock(new ThreadLock())
, m_fileLock(new FileLock(m_metaFile->getFd(), (mode & MMKV_ASHMEM)))
, m_sharedProcessLock(new InterProcessLock(m_fileLock, SharedLockType))
, m_exclusiveProcessLock(new InterProcessLock(m_fileLock, ExclusiveLockType))
, m_isInterProcess((mode & MMKV_MULTI_PROCESS) != 0 || (mode & CONTEXT_MODE_MULTI_PROCESS) != 0) {
m_actualSize = 0;
m_output = nullptr;
if (cryptKey && cryptKey->length() > 0) {
m_crypter = new AESCrypt(cryptKey->data(), cryptKey->length());
}
m_needLoadFromFile = true;
m_hasFullWriteback = false;
m_crcDigest = 0;
m_sharedProcessLock->m_enable = m_isInterProcess;
m_exclusiveProcessLock->m_enable = m_isInterProcess;
// sensitive zone
{
SCOPED_LOCK(m_sharedProcessLock);
loadFromFile();
}
}
- 1.m_mmapID MMKV的ID通過mmapedKVKey創(chuàng)建:
string mmapedKVKey(const string &mmapID, MMKVPath_t *relativePath) {
if (relativePath && g_rootDir != (*relativePath)) {
return md5(*relativePath + MMKV_PATH_SLASH + string2MMKVPath_t(mmapID));
}
return mmapID;
}
mmkvID就是經(jīng)過md5后對應緩存文件對應的路徑院仿。
- 2.m_path mmkv 緩存的路徑通過mappedKVPathWithID生成
MMKVPath_t mappedKVPathWithID(const string &mmapID, MMKVMode mode, MMKVPath_t *relativePath) {
#ifndef MMKV_ANDROID
...
#else
if (mode & MMKV_ASHMEM) {
return ashmemMMKVPathWithID(encodeFilePath(mmapID));
} else if (relativePath) {
#endif
return *relativePath + MMKV_PATH_SLASH + encodeFilePath(mmapID);
}
return g_rootDir + MMKV_PATH_SLASH + encodeFilePath(mmapID);
}
能看到這里是根據(jù)當前的mode初始化id秸抚,如果不是ashmem匿名共享內(nèi)存模式進行創(chuàng)建速和,則會和上面的處理類似歹垫。id就是經(jīng)過md5后對應緩存文件對應的路徑。
注意這里mode設(shè)置的是MMKV_ASHMEM颠放,也就是ashmem匿名共享內(nèi)存模式則是如下創(chuàng)建方法:
constexpr char ASHMEM_NAME_DEF[] = "/dev/ashmem";
MMKVPath_t ashmemMMKVPathWithID(const MMKVPath_t &mmapID) {
return MMKVPath_t(ASHMEM_NAME_DEF) + MMKV_PATH_SLASH + mmapID;
}
實際上就是在驅(qū)動目錄下的一個內(nèi)存文件地址排惨。
- 3.m_crcPath 一個.crc文件的路徑。這個crc文件實際上用于保存crc數(shù)據(jù)校驗key碰凶,避免出現(xiàn)傳輸異常的數(shù)據(jù)進行保存了暮芭。
- 4.m_file 一個依據(jù)m_path構(gòu)建的內(nèi)存文件MemoryFile對象。
- 5.m_metaFile 一個依據(jù)m_crcPath構(gòu)建的內(nèi)存文件MemoryFile對象欲低。
- 6.m_metaInfo 一個MMKVMetaInfo結(jié)構(gòu)體辕宏,這個結(jié)構(gòu)體一般是讀寫的時候,帶上的MMKV的版本信息砾莱,映射的內(nèi)存大小瑞筐,加密crc的key等。
- 7.m_crypter 默認是一個AESCrypt 對稱加密器
- 8.m_lock ThreadLock線程鎖
- 9.m_fileLock 一個以m_metaFile的fd 文件鎖
- 10.m_sharedProcessLock 類型是InterProcessLock腊瑟,這是一種文件共享鎖
- 11.m_exclusiveProcessLock 類型是InterProcessLock聚假,這是一種排他鎖
- 12.m_isInterProcess 判斷是否打開了多進程模式的標志位,一旦關(guān)閉了闰非,所有進程鎖都會失效膘格。
Ashmem匿名共享內(nèi)存
Anonymous Shared Memory-Ashmem
簡單理解:
共享內(nèi)存是Linux自帶的一種IPC機制,Android直接使用了該模型财松,不過做出了自己的改進瘪贱,進而形成了Android的匿名共享內(nèi)存(Anonymous Shared Memory-Ashmem)
應用:
APP進程同SurfaceFlinger共用一塊內(nèi)存,如此辆毡,就不需要進行數(shù)據(jù)拷貝菜秦,APP端繪制完畢,通知SurfaceFlinger端合成胚迫,再輸出到硬件進行顯示即可
更多文章
http://www.reibang.com/p/6a8513fdb792
http://www.reibang.com/p/d9bc9c668ba6
多進程MMKV實例化
多進程通信的過程
服務(wù)端創(chuàng)建MMKV實例
m_ashmemMMKV = MMKV.mmkvWithAshmemID(BenchMarkBaseService.this, id, AshmemMMKV_Size,MMKV.MULTI_PROCESS_MODE, CryptKey);
Aidl傳遞實體
ParcelableMMKV(m_ashmemMMKV);
Aidl傳遞實體 ParcelableMMKV字段
mmapID = mmkv.mmapID();
ashmemFD = mmkv.ashmemFD();
ashmemMetaFD = mmkv.ashmemMetaFD();
cryptKey = mmkv.cryptKey();
客戶端獲取傳遞實體ParcelableMMKV
AshmemMMKV ashmemMMKV = IAshmemMMKV.Stub.asInterface(service);
ParcelableMMKV parcelableMMKV = ashmemMMKV.GetAshmemMMKV();
客戶端獲取真正的操作數(shù)據(jù)的MMKV實例
parcelableMMKV.toMMKV()
public MMKV toMMKV() {
if (ashmemFD >= 0 && ashmemMetaFD >= 0) {
return MMKV.mmkvWithAshmemFD(mmapID, ashmemFD, ashmemMetaFD, cryptKey);
}
return null;
}
看一下mmkvWithAshmemFD
MMKV.mmkvWithAshmemFD(mmapID, ashmemFD, ashmemMetaFD, cryptKey);
mmkvWithAshmemFD
MMKV *MMKV::mmkvWithAshmemFD(const string &mmapID, int fd, int metaFD, string *cryptKey) {
if (fd < 0) {
return nullptr;
}
SCOPED_LOCK(g_instanceLock);
auto itr = g_instanceDic->find(mmapID);
if (itr != g_instanceDic->end()) {
MMKV *kv = itr->second;
# ifndef MMKV_DISABLE_CRYPT
kv->checkReSetCryptKey(fd, metaFD, cryptKey);
# endif
return kv;
}
auto kv = new MMKV(mmapID, fd, metaFD, cryptKey);
(*g_instanceDic)[mmapID] = kv;
return kv;
}
MMKV::MMKV(const string &mmapID, int ashmemFD, int ashmemMetaFD, string *cryptKey)
: m_mmapID(mmapID)
, m_path(mappedKVPathWithID(m_mmapID, MMKV_ASHMEM, nullptr))
, m_crcPath(crcPathWithID(m_mmapID, MMKV_ASHMEM, nullptr))
, m_dic(nullptr)
, m_dicCrypt(nullptr)
, m_file(new MemoryFile(ashmemFD))
, m_metaFile(new MemoryFile(ashmemMetaFD))
, m_metaInfo(new MMKVMetaInfo())
, m_crypter(nullptr)
, m_lock(new ThreadLock())
, m_fileLock(new FileLock(m_metaFile->getFd(), true))
, m_sharedProcessLock(new InterProcessLock(m_fileLock, SharedLockType))
, m_exclusiveProcessLock(new InterProcessLock(m_fileLock, ExclusiveLockType))
, m_isInterProcess(true) {
encode 寫入數(shù)據(jù)
encodeString
MMKV_JNI jboolean encodeString(JNIEnv *env, jobject, jlong handle, jstring oKey, jstring oValue) {
MMKV *kv = reinterpret_cast<MMKV *>(handle);
if (kv && oKey) {
string key = jstring2string(env, oKey);
if (oValue) {
string value = jstring2string(env, oValue);
return (jboolean) kv->set(value, key);
} else {
kv->removeValueForKey(key);
return (jboolean) true;
}
}
return (jboolean) false;
}
bool MMKV::set(const string &value, MMKVKey_t key) {
if (isKeyEmpty(key)) {
return false;
}
auto data = MiniPBCoder::encodeDataWithObject(value);
return setDataForKey(std::move(data), key);
}
- 1.encodeDataWithObject 編碼壓縮內(nèi)容
- 2.setDataForKey 保存數(shù)據(jù)
setDataForKey
保存數(shù)據(jù)到映射的文件
bool MMKV::setDataForKey(MMBuffer &&data, MMKVKey_t key) {
if (data.length() == 0 || isKeyEmpty(key)) {
return false;
}
SCOPED_LOCK(m_lock);
SCOPED_LOCK(m_exclusiveProcessLock);
checkLoadData();
auto ret = appendDataWithKey(data, key);
if (ret) {
m_dic[key] = std::move(data);
m_hasFullWriteback = false;
}
return ret;
}
設(shè)置了互斥鎖喷户,和線程鎖。整個步驟分為兩步驟:
- 1.checkLoadData 保存數(shù)據(jù)之前访锻,校驗已經(jīng)存儲的數(shù)據(jù)
- 2.appendDataWithKey 進行數(shù)據(jù)的保存
appendDataWithKey
bool MMKV::appendDataWithKey(const MMBuffer &data, MMKVKey_t key) {
size_t keyLength = key.length();
// size needed to encode the key
size_t size = keyLength + pbRawVarint32Size((int32_t) keyLength);
// size needed to encode the value
size += data.length() + pbRawVarint32Size((int32_t) data.length());
SCOPED_LOCK(m_exclusiveProcessLock);
bool hasEnoughSize = ensureMemorySize(size);
if (!hasEnoughSize || !isFileValid()) {
return false;
}
m_output->writeString(key);
m_output->writeData(data); // note: write size of data
auto ptr = (uint8_t *) m_file->getMemory() + Fixed32Size + m_actualSize;
if (m_crypter) {
m_crypter->encrypt(ptr, ptr, size);
}
m_actualSize += size;
updateCRCDigest(ptr, size);
return true;
}
判斷是否有足夠的空間褪尝,沒有則調(diào)用ensureMemorySize進行擴容闹获,實在無法從內(nèi)存中映射出來,那說明系統(tǒng)沒空間了就返回異常河哑。
正常情況下避诽,是往全局緩沖區(qū)CodedOutputData 先后在文件內(nèi)存的末尾寫入key和value的數(shù)據(jù)。并對這部分的數(shù)據(jù)進行一次加密璃谨,最后更新這個存儲區(qū)域的crc校驗碼沙庐。
這里實際上是調(diào)用了CodedOutputData的writeString把數(shù)據(jù)保存到映射的內(nèi)存中。
void CodedOutputData::writeString(const string &value) {
size_t numberOfBytes = value.size();
this->writeRawVarint32((int32_t) numberOfBytes);
if (m_position + numberOfBytes > m_size) {
auto msg = "m_position: " + to_string(m_position) + ", numberOfBytes: " + to_string(numberOfBytes) +
", m_size: " + to_string(m_size);
throw out_of_range(msg);
}
memcpy(m_ptr + m_position, ((uint8_t *) value.data()), numberOfBytes);
m_position += numberOfBytes;
}
decode MMKV讀取數(shù)據(jù)
MMKV讀取數(shù)據(jù)
MMKV_JNI jstring decodeString(JNIEnv *env, jobject obj, jlong handle, jstring oKey, jstring oDefaultValue) {
MMKV *kv = reinterpret_cast<MMKV *>(handle);
if (kv && oKey) {
string key = jstring2string(env, oKey);
string value;
bool hasValue = kv->getString(key, value);
if (hasValue) {
return string2jstring(env, value);
}
}
return oDefaultValue;
}
bool MMKV::getString(MMKVKey_t key, string &result) {
if (isKeyEmpty(key)) {
return false;
}
SCOPED_LOCK(m_lock);
auto &data = getDataForKey(key);
if (data.length() > 0) {
try {
result = MiniPBCoder::decodeString(data);
return true;
} catch (std::exception &exception) {
MMKVError("%s", exception.what());
}
}
return false;
}
大致可以分分為兩步:
- 1.getDataForKey 通過key找緩存的數(shù)據(jù)
- 2.decodeString 對獲取到的數(shù)據(jù)進行解碼
getDataForKey
const MMBuffer &MMKV::getDataForKey(MMKVKey_t key) {
checkLoadData();
auto itr = m_dic.find(key);
if (itr != m_dic.end()) {
return itr->second;
}
static MMBuffer nan;
return nan;
}
由于是一個多進程的組件佳吞,因此每一次進行讀寫之前都需要進行一次checkLoadData的校驗拱雏。而這個方法從上文可知,通過crc校驗碼底扳,寫回計數(shù)铸抑,文件長度來判斷文件是否發(fā)生了變更,是否追加刪除數(shù)據(jù)衷模,從而是否需要重新充內(nèi)存文件中獲取數(shù)據(jù)緩存到m_dic鹊汛。
也因此,在getDataForKey方法中阱冶,可以直接從m_dic中通過key找value刁憋。
decodeString
string MiniPBCoder::decodeString(const MMBuffer &oData) {
MiniPBCoder oCoder(&oData);
return oCoder.decodeOneString();
}
string MiniPBCoder::decodeOneString() {
return m_inputData->readString();
}
string CodedInputData::readString() {
int32_t size = readRawVarint32();
if (size < 0) {
throw length_error("InvalidProtocolBuffer negativeSize");
}
auto s_size = static_cast<size_t>(size);
if (s_size <= m_size - m_position) {
string result((char *) (m_ptr + m_position), s_size);
m_position += s_size;
return result;
} else {
throw out_of_range("InvalidProtocolBuffer truncatedMessage");
}
}
能看到實際上很簡單就是從m_dic找到對應的MMBuffer數(shù)據(jù),此時的可以通過CodedInputData對MMBuffer對應的內(nèi)存塊(已經(jīng)知道內(nèi)存起始地址木蹬,長度)進行解析數(shù)據(jù)至耻。
總結(jié)
MMKV讀寫是直接讀寫到mmap文件映射的內(nèi)存上,繞開了普通讀寫io需要進入內(nèi)核届囚,寫到磁盤的過程有梆。光是這種級別優(yōu)化,都可以拉開三個數(shù)量級的性能差距意系。但是也誕生了一個很大的問題泥耀,一個進程在32位的機子中有4g的虛擬內(nèi)存限制,而我們把文件映射到虛擬內(nèi)存中蛔添,如果文件過大虛擬內(nèi)存就會出現(xiàn)大量的消耗最后出現(xiàn)異常痰催,對于不熟悉Linux的朋友就無法理解這種現(xiàn)象。
有幾個關(guān)于MMKV使用的注意事項:
- 1.保證每一個文件存儲的數(shù)據(jù)都比較小迎瞧,也就說需要把數(shù)據(jù)根據(jù)業(yè)務(wù)線存儲分散夸溶。這要就不會把虛擬內(nèi)存消耗過快。
- 2.還需要在適當?shù)臅r候釋放一部分內(nèi)存數(shù)據(jù)凶硅,比如在App中監(jiān)聽onTrimMemory方法缝裁,在Java內(nèi)存吃緊的情況下進行MMKV的trim操作(不準確,我們暫時以此為信號足绅,最好自己監(jiān)聽進程中內(nèi)存使用情況)捷绑。
- 2.在不需要使用的時候韩脑,最好把MMKV給close掉。甚至調(diào)用exit方法粹污。
參考文章:http://www.reibang.com/p/c12290a9a3f7
官方Demo:https://github.com/Tencent/MMKV/tree/master/Android/MMKV