面試官: IO優化是怎麼做的,使用 SharedPreferences爲什麼這麼卡,mmkv原理是什麼
心理分析:IO優化一直是每個企業必選項,每次聞到都很頭疼,面試官想問有沒有相關經驗,如果有的話,只有兩種答案sqlitedatabse, SharedPreferences。 這兩個很常見,肯定不是面試官想問的。 那只有一種答案了,對,就是最新的mmkv框架
接下來,會問你他的原理 你是怎麼看。 它的優缺點。爲什麼比其他的好。從原理層來解析。這纔是最難的。
這篇文章 從原理層說明他們的區別
更多面試內容,面試專題,flutter視頻 全套,音視頻從0到高手開發。
關注GitHub:https://github.com/xiangjiana/Android-MS
免費獲取面試PDF合集
1.1 MMKV的概念
mkkv是基於 mmap 的高性能通用 key-value 組件,底層序列化/反序列化使用 protobuf 實現,性能高,穩定性強。
mmkv github下載地址
- MMKV 是基於 mmap 內存映射的移動端通用 key-value 組件,底層序列化/反序列化使用 protobuf 實現,性能高,穩定性強。
- 從 2015 年中至今,在Android 微信上使用已有近 3 年,其性能和穩定性經過了時間的驗證。在騰訊內部開源半年之後,得到公司內部團隊的廣泛應用和一致好評。
- 通過 mmap 內存映射文件,提供一段可供隨時寫入的內存塊,App 只管往裏面寫數據,
由操作系統負責將內存回寫到文件,不必擔心 crash 導致數據丟失。 - XML、JSON 更注重數據結構化,關注人類可讀性和語義表達能力。 ProtoBuf 更注重數據序列化,關注效率、空間、速度,人類可讀性差,語義表達能力不足(爲保證極致的效率,會捨棄一部分元信息)
1.2 特點
- 高性能 實時寫入
- 穩定 防crash
- 多進程訪問
通過與 Android 開發同學的溝通,瞭解到系統自帶的 SharedPreferences 對多進程的支持不好。
現有基於 ContentProvider 封裝的實現,雖然多進程是支持了,但是性能低下,經常導致 ANR。
考慮到 mmap 共享內存本質上的多進程共享的,我們在這個基礎上,深入挖掘了 Android 系統的能力,提供了可能是業界最高效的多進程數據共享組件。 - 匿名內存
在多進程共享的基礎上,考慮到某些敏感數據(例如密碼)需要進程間共享,但是不方便落地存儲到文件上,直接用 mmap 不合適。
我們瞭解到 Android 系統提供了 Ashmem 匿名共享內存的能力,發現它在進程退出後就會消失,不會落地到文件上,非常適合這個場景。
我們很愉快地提供了 Ashmem MMKV 的功能。 - 數據加密
不像 iOS 提供了硬件層級的加密機制,在 Android 環境裏,數據加密是非常必須的。
MMKV 使用了 AES CFB-128 算法來加密/解密。我們選擇 CFB 而不是常見的 CBC 算法,
主要是因爲 MMKV 使用 append-only 實現插入/更新操作,流式加密算法更加合適。 - 數據有效性
1.3性能對比
我們將 MMKV 和 SharedPreferences、SQLite 進行對比, 重複讀寫操作 1k 次。相關測試代碼在 Android/MMKV/mmkvdemo/。結果如下圖表。
單進程性能
可見,MMKV 在寫入性能上遠遠超越 SharedPreferences & SQLite,在讀取性能上也有相近或超越的表現。
可見,MMKV 無論是在寫入性能還是在讀取性能,都遠遠超越 MultiProcessSharedPreferences & SQLite & SQLite, MMKV 在 Android 多進程 key-value 存儲組件上是不二之選。
1.3 MMKV 原理
-
內存準備 通過 mmap 內存映射文件,提供一段可供隨時寫入的內存塊,App 只管往裏面寫數據,由操作系統負責將內存回寫到文件,不必擔心 crash 導致數據丟失。
-
數據組織 數據序列化方面我們選用 protobuf 協議,pb 在性能和空間佔用上都有不錯的表現。
-
寫入優化 考慮到主要使用場景是頻繁地進行寫入更新,我們需要有增量更新的能力。我們考慮將增量 kv 對象序列化後,append 到內存末尾。 這樣同一個 key 會有新舊若干份數據,最新的數據在最後;那麼只需在程序啓動第一次打開 mmkv 時,不斷用後讀入的 value 替換之前的值,就可以保證數據是最新有效的。
-
空間增長 使用 append 實現增量更新帶來了一個新的問題,就是不斷 append 的話,文件大小會增長得不可控。我們需要在性能和空間上做個折中。 以內存 pagesize 爲單位申請空間,在空間用盡之前都是 append 模式;當 append 到文件末尾時,進行文件重整、key 排重,嘗試序列化保存排重結果; 排重後空間還是不夠用的話,將文件擴大一倍,直到空間足夠。
-
數據有效性 考慮到文件系統、操作系統都有一定的不穩定性,我們另外增加了 crc 校驗,對無效數據進行甄別。
更詳細的設計原理參考 MMKV 原理。
1.4 快速上手
dependencies {
implementation 'com.tencent:mmkv:1.0.23'
// replace "1.0.23" with any available version
}
MMKV的使用非常簡單, 所有變更立馬生效,無需調用 sync、apply。 在 App 啓動時初始化 MMKV,設定 MMKV 的根目錄 (默認/data/data/xxx.xxx/files/mmkv/) (sp存儲在/data/data/xxx.xxx/shared_prefs/)
支持從SP遷移數據importFromSharedPreferences
MMKV 還額外實現了一遍 SharedPreferences、SharedPreferences.Editor 這兩個 interface
// 可以跟SP用法一樣
SharedPreferences.Editor editor = mmkv.edit();
// 無需調用 commit()
//editor.commit();
MMKV 的使用非常簡單,所有變更立馬生效,無需調用 sync、apply。 在 App 啓動時初始化 MMKV,設定 MMKV 的根目錄(files/mmkv/),例如在 MainActivity 裏:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String rootDir = MMKV.initialize(this);
System.out.println("mmkv root: " + rootDir);
//……
}
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");
使用完畢的幾個方法
public native void clearAll();
// MMKV's size won't reduce after deleting key-values
// call this method after lots of deleting f you care about disk usage
// note that `clearAll` has the similar effect of `trim`
public native void trim();
// call this method if the instance is no longer needed in the near future
// any subsequent call to the instance is undefined behavior
public native void close();
// call on memory warning
// any subsequent call to the instance will load all key-values from file again
public native void clearMemoryCache();
// you don't need to call this, really, I mean it
// unless you care about out of battery
public void sync() {
sync(true);
}
1.5 補充適用建議
如果使用請務必做code19版本的適配,這個在github官網有說明
依賴下面這個庫,然後對19區分處理
implementation ‘com.getkeepsafe.relinker:relinker:1.3.1’
if (android.os.Build.VERSION.SDK_INT == 19) {
MMKV.initialize(relativePath, new MMKV.LibLoader() {
@Override
public void loadLibrary(String libName) {
ReLinker.loadLibrary(context, libName);
}
});
} else {
MMKV.initialize(context);
}
1.6 限制
可看到,一個鍵會存入多分實例,最後存入的就是最新的。
MMKV 在大部分情況下都性能強勁,key/value 的數量和長度都沒有限制。
然而 MMKV 在內存裏緩存了所有的 key-value,在總大小比較大的情況下(例如 100M+),App 可能會爆內存,觸發重整回寫時,寫入速度也會變慢。
支持大文件的 MMKV 正在開發中,有望在下一個大版本發佈。
1.7 多進程使用
1.7.1鎖 lock unlock tryLock
注意如果一個進程lock住,另一個進程mmkvWithID獲取MMKV時就阻塞住,直到持有進程釋放。
// get the lock immediately
MMKV mmkv2 = MMKV.mmkvWithID(LOCK_PHASE_2, MMKV.MULTI_PROCESS_MODE);
mmkv2.lock();
Log.d("locked in child", LOCK_PHASE_2);
Runnable waiter = new Runnable() {
@Override
public void run() {
//阻塞住 直到其他進程釋放
MMKV mmkv1 = MMKV.mmkvWithID(LOCK_PHASE_1, MMKV.MULTI_PROCESS_MODE);
mmkv1.lock();
Log.d("locked in child", LOCK_PHASE_1);
}
};
注意:如果其他進程有進行修改,不會立即觸發onContentChangedByOuterProcess,
checkLoadData如果變化,會clearMemoryState,重新loadFromFile。//數據量大時不要太頻繁
讀取decodeXXX會阻塞住,先回調onContentChangedByOuterProcess,再返回值,保證值是最新的。
1.7.2 mmkvWithAshmemID 匿名共享內存
可以進行進程間通信,可設置pageSize
// a memory only MMKV, cleared on program exit
// size cannot change afterward (because ashmem won't allow it)
1.7.3 測試結果
write速度: mmkv > cryptKV >> sp
read速度: sp > cryptKV > mmkv
1.8 Binder MMAP(一次拷貝)
Linux的內存分用戶空間跟內核空間,同時頁表有也分兩類,用戶空間頁表跟內核空間頁表,每個進程有一個用戶空間頁表,但是系統只有一個內核空間頁表。
而Binder mmap的關鍵是:更新用戶空間對應的頁表的同時也同步映射內核頁表,讓兩個頁表都指向同一塊地址,
這樣一來,數據只需要從A進程的用戶空間,直接拷貝到B所對應的內核空間,而B多對應的內核空間在B進程的用戶空間也有相應的映射,這樣就無需從內核拷貝到用戶空間了。
copy_from_user() //將數據從用戶空間拷貝到內核空間
copy_to_user() //將數據從內核空間拷貝到用戶空間
1.8.1 Liunx進程隔離
1.8.2 傳統IPC
1.8.3 Binder通信
1.9 普通文件mmap原理
普通文件的訪問方式有兩種:
- 第一種是通過read/write系統調訪問,先在用戶空間分配一段buffer,然後,進入內核,將內容從磁盤讀 取到內核緩衝,最後,拷貝到用戶進程空間,至少牽扯到兩次數據拷貝;
同時,多個進程同時訪問一個文件,每個進程都有一個副本,存在資源浪費的問題。 - 另一種是通過mmap來訪問文件,mmap()將文件直接映射到用戶空間,文件在mmap的時候,內存並未 真正分配,
只有在第一次讀取/寫入的時候纔會觸發,這個時候,會引發缺頁中斷,在處理缺頁中斷的時候,完成內存也分配,同時也完成文件數據的拷貝。
並且,修改用戶空間對應的頁表,完成到物理內存到用戶空間的映射,這種方式只存在一次數據拷貝,效率更高。
同時多進程間通過mmap共享文件數據的時候,僅需要一塊物理內存就夠了。
Android中使用mmap,可以通過RandomAccessFile與MappedByteBuffer來配合。
通過randomAccessFile.getChannel().map獲取到MappedByteBuffer。然後調用ByteBuffer的put方法添加數據。
RandomAccessFile randomAccessFile = new RandomAccessFile("path","rw");
MappedByteBuffer mappedByteBuffer=
randomAccessFile.getChannel().map(FileChannel.MapMode.READ_WRITE,0,
mappedByteBuffer.putChar('c');
mappedByteBuffer.getChar();
更多面試內容,面試專題,flutter視頻 全套,音視頻從0到高手開發。
關注GitHub:https://github.com/xiangjiana/Android-MS
免費獲取面試PDF合集