開篇廢話
開局一張圖,說明一切問題。

MMKV優(yōu)勢
可以看出MMKV相比SP的優(yōu)勢還是比較大的,除了需要引入庫,有一些修改上的成本以外,就沒有什么能夠阻擋MMKV了。當然了,MMKV也有著不廣為人知的缺點,放在最后。
MMKV還直接支持了將SharedPreferences的歷史數(shù)據(jù)轉換為MMKV進行存儲,只不過需要注意一點,不可回退。
且聽我慢慢道來
SP具體存在哪些問題
- 容易anr,無論是commit、apply、getxxx都可能導致ANR。
SharedPreferences 本身是一個接口,其具體的實現(xiàn)類是 SharedPreferencesImpl,而 Context 的各個和 SharedPreferences 相關的方法則是由 ContextImpl 來實現(xiàn)的。而每當我們獲取到一個 SharedPreferences 對象時,這個對象將一直被保存在內(nèi)存當中,如果SP文件過大,那么會對內(nèi)存的占用是有很大的影響的。
如果SP文件過大的話,在App啟動的時候也會造成啟動慢,甚至ANR的。
class ContextImpl extends Context {
//根據(jù)應用包名緩存所有 SharedPreferences,根據(jù) xmlFile 和具體的 SharedPreferencesImpl 對應上
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
//根據(jù) fileName 拿到對應的 xmlFile
private ArrayMap<String, File> mSharedPrefsPaths;
}
如果我們在初始化 SharedPreferencesImpl 后緊接著就去 getValue 的話,勢必也需要確保子線程已經(jīng)加載完成后才去進行取值操作。SharedPreferencesImpl 就通過在每個 getValue 方法中調用 awaitLoadedLocked()方法來判斷是否需要阻塞外部線程,確保取值操作一定會在子線程執(zhí)行完畢后才執(zhí)行。loadFromDisk()方法會在任務執(zhí)行完畢后調用 mLock.notifyAll()喚醒所有被阻塞的線程。所以說,如果 SharedPreferences 存儲的數(shù)據(jù)量很大的話,那么就有可能導致外部的調用者線程被阻塞,嚴重時甚至可能導致 ANR。當然,這種可能性也只是發(fā)生在加載磁盤文件完成之前,當加載完成后 awaitLoadedLocked()方法自然不會阻塞線程。這也是為什么第一次寫入或者讀取sp相比mmkv慢十多倍最主要的原因。
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
//判斷是否需要讓外部線程等待
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
@GuardedBy("mLock")
private void awaitLoadedLocked() {
if (!mLoaded) {
// Raise an explicit StrictMode onReadFromDisk for this
// thread, since the real read will be in a different
// thread and otherwise ignored by StrictMode.
BlockGuard.getThreadPolicy().onReadFromDisk();
}
while (!mLoaded) {
try {
//還未加載線程,讓外部線程暫停等待
mLock.wait();
} catch (InterruptedException unused) {
}
}
if (mThrowable != null) {
throw new IllegalStateException(mThrowable);
}
}
private void loadFromDisk() {
···
synchronized (mLock) {
mLoaded = true;
mThrowable = thrown;
// It's important that we always signal waiters, even if we'll make
// them fail with an exception. The try-finally is pretty wide, but
// better safe than sorry.
try {
if (thrown == null) {
if (map != null) {
mMap = map;
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
}
// In case of a thrown exception, we retain the old map. That allows
// any open editors to commit and store updates.
} catch (Throwable t) {
mThrowable = t;
} finally {
//喚醒所有被阻塞的線程
mLock.notifyAll();
}
}
}
- SP數(shù)據(jù)保存的格式為xml。相比ProtoBuffer來說,性能較弱。
之前也是做過ProtoBuffer的原理,首先我們知道ProtoBuffer體積非常小,所以在存儲上就占據(jù)了很大的優(yōu)勢。MMKV底層序列化和反序列化是ProtoBuffer實現(xiàn)的,所以在存儲速度上也有著很大的優(yōu)勢。 - 每次寫入數(shù)據(jù)的時候是全量寫入。假如xml有100條數(shù)據(jù),當插入一條新的數(shù)據(jù)或者更新一條數(shù)據(jù),SP會將全部的數(shù)據(jù)全部重新寫入文件,這是造成SP寫入慢的原因。
- 當保存的數(shù)據(jù)較多時,會在進程中占用過多的內(nèi)存。
commit() 和 apply() 兩個方法都會通過調用 commitToMemory() 方法拿到修改后的全量數(shù)據(jù)commitToMemory(),SharedPreferences 包含的所有鍵值對數(shù)據(jù)都存儲在 mapToWriteToDisk 中,Editor 改動到的所有鍵值對數(shù)據(jù)都存儲在 mModified 中。如果 mClear 為 true,則會先清空 mapToWriteToDisk,然后再遍歷 mModified,將 mModified 中的所有改動都同步給 mapToWriteToDisk。最終 mapToWriteToDisk 就保存了要重新寫入到磁盤文件中的全量數(shù)據(jù),SharedPreferences 會根據(jù) mapToWriteToDisk 完全覆蓋掉舊的 xml 文件。
// Returns true if any changes were made
private MemoryCommitResult commitToMemory() {
long memoryStateGeneration;
boolean keysCleared = false;
List<String> keysModified = null;
Set<OnSharedPreferenceChangeListener> listeners = null;
Map<String, Object> mapToWriteToDisk;
synchronized (SharedPreferencesImpl.this.mLock) {
// We optimistically don't make a deep copy until
// a memory commit comes in when we're already
// writing to disk.
if (mDiskWritesInFlight > 0) {
// We can't modify our mMap as a currently
// in-flight write owns it. Clone it before
// modifying it.
// noinspection unchecked
mMap = new HashMap<String, Object>(mMap);
}
//拿到內(nèi)存中的全量數(shù)據(jù)
mapToWriteToDisk = mMap;
mDiskWritesInFlight++;
boolean hasListeners = mListeners.size() > 0;
if (hasListeners) {
keysModified = new ArrayList<String>();
listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
}
synchronized (mEditorLock) {
//用于標記最終是否改動到了 mapToWriteToDisk
boolean changesMade = false;
if (mClear) {
if (!mapToWriteToDisk.isEmpty()) {
changesMade = true;
//清空所有在內(nèi)存中的數(shù)據(jù)
mapToWriteToDisk.clear();
}
keysCleared = true;
//恢復狀態(tài),避免二次修改時狀態(tài)錯位
mClear = false;
}
for (Map.Entry<String, Object> e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
// "this" is the magic value for a removal mutation. In addition,
// setting a value to "null" for a given key is specified to be
// equivalent to calling remove on that key.
if (v == this || v == null) { //意味著要移除該鍵值對
if (!mapToWriteToDisk.containsKey(k)) {
continue;
}
mapToWriteToDisk.remove(k);
} else { //對應修改鍵值對值的情況
if (mapToWriteToDisk.containsKey(k)) {
Object existingValue = mapToWriteToDisk.get(k);
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
//只有在的確是修改了或新插入鍵值對的情況才需要保存值
mapToWriteToDisk.put(k, v);
}
changesMade = true;
if (hasListeners) {
keysModified.add(k);
}
}
//恢復狀態(tài),避免二次修改時狀態(tài)錯位
mModified.clear();
if (changesMade) {
mCurrentMemoryStateGeneration++;
}
memoryStateGeneration = mCurrentMemoryStateGeneration;
}
}
return new MemoryCommitResult(memoryStateGeneration, keysCleared, keysModified,
listeners, mapToWriteToDisk);
}
- 不支持多進程模式,想實現(xiàn)需要配合跨進程通訊。
如果想要實現(xiàn)多進程共享數(shù)據(jù),就需要自己去實現(xiàn)跨進程通訊,比如ContentProvider、AIDL、或者自己直接實現(xiàn)Binder等方式。
MMKV的優(yōu)點
- MMKV實現(xiàn)了SharedPreferences接口,基本可以無縫切換。
MMKV提供了API可以直接將SP存儲的內(nèi)容直接轉向MMKV存儲,不可回退。
SharedPreferences sources = context.getSharedPreferences(name, mode);
mmkv.importFromSharedPreferences(sources);
- 通過mmap映射文件,通過一次拷貝。
通過 mmap 內(nèi)存映射文件,提供一段可供隨時寫入的內(nèi)存塊,App 只管往里面寫數(shù)據(jù),由操作系統(tǒng)負責將內(nèi)存回寫到文件,不必擔心 crash 導致數(shù)據(jù)丟失。通過內(nèi)存映射實現(xiàn)了文件到用戶空間只需要一次拷貝,而SP則需要兩次拷貝。
mmap 是 linux 提供的一種內(nèi)存映射文件的方法,即將一個文件或者其他對象映射到進程的地址空間,實現(xiàn)文件磁盤地址和進程虛擬地址空間中一段虛擬地址的一一對應關系;實現(xiàn)這樣的映射關系后,進程就可以采用指針的方式讀寫操作這一塊內(nèi)存,而系統(tǒng)會自動回寫臟頁面到對應的文件磁盤上,即完成了對文件的操作而不必調用read,write等系統(tǒng)調用函數(shù)。
Binder 的底層也是通過了 mmap 來實現(xiàn)一次內(nèi)存拷貝的多進程通訊,所以MMKV也不用擔心多進程下的數(shù)據(jù)持久化。 - MMKV數(shù)據(jù)存儲序列化方面選用 protobuf 協(xié)議。
該協(xié)議類比xml有如下幾個有點:- 語言無關、平臺無關。即 ProtoBuf 支持 Java、C++、Python 等多種語言,支持多個平臺
- 高效。即比 XML 更?。? ~ 10倍)、更快(20 ~ 100倍)、更為簡單
- 擴展性、兼容性好。你可以更新數(shù)據(jù)結構,而不影響和破壞原有的舊程序
- MMKV是增量更新,有性能優(yōu)勢。
增量 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 排重,嘗試序列化保存排重結果;排重后空間還是不夠用的話,將文件擴大一倍,直到空間足夠。
MMKV的缺點
- 由上可知,Linux 采用了分頁來管理內(nèi)存,存入數(shù)據(jù)先要創(chuàng)建一個文件,并要給這個文件分配一個固定的大小。如果存入了一個很小的數(shù)據(jù),那么這個文件其余的內(nèi)存就會被浪費。相反如果存入的數(shù)據(jù)比文件大,就需要動態(tài)擴容。
- 還有一點就是 SP 轉 MMKV 簡單,如果想要再將 MMKV 轉換為其它方式的話,現(xiàn)在是不支持的。如果哪一天 Jetpack DataStore 崛起了,遷移起來可能會比較麻煩。
如何替換并且兼容
如何替換才能更好的兼容之前的代碼呢?直接上代碼,代碼很簡單,一看就懂。
dependencies {
implementation 'com.tencent:mmkv:1.2.7'
implementation 'com.getkeepsafe.relinker:relinker:1.4.4'
}
import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import android.text.TextUtils;
import androidx.annotation.Nullable;
import com.getkeepsafe.relinker.ReLinker;
import com.tencent.mmkv.MMKV;
import com.tencent.mmkv.MMKVLogLevel;
import java.util.Set;
/**
* Created by guoshichao on 2022/1/12
* 替換SharedPreferences為MMKV
*/
public class MySharedPreferences {
public static MySharedPreferences getDefaultSharedPreferences() {
Context context = MyApplication.getAppContext();
String defaultName = context.getPackageName() + "_preferences";
return new MySharedPreferences(context, defaultName, Context.MODE_PRIVATE);
}
public static MySharedPreferences getSharedPreferences(String name) {
return new MySharedPreferences(MyApplication.getAppContext(), name, Context.MODE_PRIVATE);
}
public static MySharedPreferences getSharedPreferences(String name, int mode) {
return new MySharedPreferences(null, name, mode);
}
public static MySharedPreferences getSharedPreferences(Context context, String name, int mode) {
return new MySharedPreferences(context, name, mode);
}
/**
* WRITE_TO_MMKV 為ture表示數(shù)據(jù)寫入MMKV,為false,表示數(shù)據(jù)從MMKV寫入SharedPreferences
*/
private static boolean mMMKVEnabled = true;
public static void setMMKVEnable(boolean enable) {
mMMKVEnabled = enable;
}
public static boolean isMMKVEnable() {
return mMMKVEnabled;
}
private MMKV mmkv, defaultMMKV;
private SharedPreferences spData;
private SharedPreferences.Editor spEditor;
private static boolean mmkvInited = false;
public static void initMMKV(Application app) {
if (mmkvInited) {
return;
}
mmkvInited = true;
if (MySharedPreferences.isMMKVEnable()) {
String root = app.getFilesDir().getAbsolutePath() + "/mmkv";
MMKVLogLevel logLevel = MyApplication.isDebuging() ? MMKVLogLevel.LevelDebug : MMKVLogLevel.LevelError;
try {
MMKV.initialize(root, new MMKV.LibLoader() {
@Override
public void loadLibrary(String libName) {
try {
ReLinker.loadLibrary(app, libName);
} catch (Throwable ex) {
MySharedPreferences.setMMKVEnable(false);
}
}
}, logLevel);
} catch (Throwable ex) {
MySharedPreferences.setMMKVEnable(false);
}
}
}
private MySharedPreferences(Context context, String name, int mode) {
if (mMMKVEnabled) {
try {
MMKV.initialize(MyApplication.getAppContext());
this.mmkv = MMKV.mmkvWithID(name);
this.defaultMMKV = MMKV.defaultMMKV();
} catch (IllegalArgumentException iae) {
String message = iae.getMessage();
if (!TextUtils.isEmpty(message) && message.contains("Opening a multi-process MMKV")) {
try {
this.mmkv = MMKV.mmkvWithID(name, MMKV.MULTI_PROCESS_MODE);
this.defaultMMKV = MMKV.defaultMMKV(MMKV.MULTI_PROCESS_MODE, null);
} catch (Throwable ex) {
//如果出現(xiàn)異常拋埋點給服務端
MyStatistics.getEvent().eventNormal("MMKV", 0, 102, name);
return;
}
}
} catch (Throwable ex) {
//如果出現(xiàn)異常拋埋點給服務端
MyStatistics.getEvent().eventNormal("MMKV", 0, 101, name);
return;
}
}
if (null == context) {
context = MyApplication.getAppContext();
}
if (null != context) {
if (mMMKVEnabled) {
if (null != defaultMMKV && !defaultMMKV.contains(name)) {
SharedPreferences sources = context.getSharedPreferences(name, mode);
mmkv.importFromSharedPreferences(sources);
defaultMMKV.encode(name, true);
Logger.i("MySharedPreferences", "transform SP-" + name + " to MMKV");
}
} else {
spData = context.getSharedPreferences(name, mode);
}
}
}
public final class Editor {
public Editor putString(String key, @Nullable String value) {
if (mMMKVEnabled) {
if (null != mmkv) {
mmkv.encode(key, value);
}
} else {
if (null != spEditor) {
spEditor.putString(key, value);
}
}
return this;
}
public Editor putStringSet(String key, @Nullable Set<String> values) {
if (mMMKVEnabled) {
if (null != mmkv) {
mmkv.encode(key, values);
}
} else {
if (null != spEditor) {
spEditor.putStringSet(key, values);
}
}
return this;
}
public Editor putInt(String key, int value) {
if (mMMKVEnabled) {
if (null != mmkv) {
mmkv.encode(key, value);
}
} else {
if (null != spEditor) {
spEditor.putInt(key, value);
}
}
return this;
}
public Editor putLong(String key, long value) {
if (mMMKVEnabled) {
if (null != mmkv) {
mmkv.encode(key, value);
}
} else {
if (null != spEditor) {
spEditor.putLong(key, value);
}
}
return this;
}
public Editor putFloat(String key, float value) {
if (mMMKVEnabled) {
if (null != mmkv) {
mmkv.encode(key, value);
}
} else {
if (null != spEditor) {
spEditor.putFloat(key, value);
}
}
return this;
}
public Editor putBoolean(String key, boolean value) {
if (mMMKVEnabled) {
if (null != mmkv) {
mmkv.encode(key, value);
}
} else {
if (null != spEditor) {
spEditor.putBoolean(key, value);
}
}
return this;
}
public Editor remove(String key) {
if (mMMKVEnabled) {
if (null != mmkv) {
mmkv.removeValueForKey(key);
}
} else {
if (null != spEditor) {
spEditor.remove(key);
}
}
return this;
}
public Editor clear() {
if (mMMKVEnabled) {
if (null != mmkv) {
mmkv.clearAll();
}
} else {
if (null != spEditor) {
spEditor.clear();
}
}
return this;
}
/**
* 無實際意義,只是為了適配以前已經(jīng)調用了commit的舊的方式
*/
public boolean commit() {
if (!mMMKVEnabled) {
if (null != spEditor) {
return spEditor.commit();
}
}
return true;
}
/**
* 無實際意義,只是為了適配以前已經(jīng)調用了apply的舊的方式
*/
public void apply() {
if (!mMMKVEnabled) {
if (null != spEditor) {
spEditor.apply();
}
}
}
}
public MySharedPreferences.Editor edit() {
if (!mMMKVEnabled) {
spEditor = spData.edit();
}
return new Editor();
}
@Nullable
public String getString(String key, @Nullable String defValue) {
if (mMMKVEnabled) {
if (null != mmkv) {
return mmkv.getString(key, defValue);
}
} else {
if (null != spData) {
return spData.getString(key, defValue);
}
}
return defValue;
}
@Nullable
Set<String> getStringSet(String key, @Nullable Set<String> defValues) {
if (mMMKVEnabled) {
if (null != mmkv) {
return mmkv.getStringSet(key, defValues);
}
} else {
if (null != spData) {
return spData.getStringSet(key, defValues);
}
}
return defValues;
}
public int getInt(String key, int defValue) {
if (mMMKVEnabled) {
if (null != mmkv) {
return mmkv.getInt(key, defValue);
}
} else {
if (null != spData) {
return spData.getInt(key, defValue);
}
}
return defValue;
}
public long getLong(String key, long defValue) {
if (mMMKVEnabled) {
if (null != mmkv) {
return mmkv.getLong(key, defValue);
}
} else {
if (null != spData) {
return spData.getLong(key, defValue);
}
}
return defValue;
}
public float getFloat(String key, float defValue) {
if (mMMKVEnabled) {
if (null != mmkv) {
return mmkv.getFloat(key, defValue);
}
} else {
if (null != spData) {
return spData.getFloat(key, defValue);
}
}
return defValue;
}
public boolean getBoolean(String key, boolean defValue) {
if (mMMKVEnabled) {
if (null != mmkv) {
return mmkv.getBoolean(key, defValue);
}
} else {
if (null != spData) {
return spData.getBoolean(key, defValue);
}
}
return defValue;
}
public boolean contains(String key) {
if (mMMKVEnabled) {
if (null != mmkv) {
return mmkv.containsKey(key);
}
} else {
if (null != spData) {
return spData.contains(key);
}
}
return false;
}
}
寫到最后
最后,最重要的就是MMKV的缺點,遷移到MMKV是不可逆操作,一定要慎重。