開(kāi)篇廢話
開(kāi)局一張圖姻政,說(shuō)明一切問(wèn)題但汞。
可以看出MMKV相比SP的優(yōu)勢(shì)還是比較大的宿刮,除了需要引入庫(kù),有一些修改上的成本以外私蕾,就沒(méi)有什么能夠阻擋MMKV了僵缺。當(dāng)然了,MMKV也有著不廣為人知的缺點(diǎn)踩叭,放在最后磕潮。
MMKV還直接支持了將SharedPreferences的歷史數(shù)據(jù)轉(zhuǎn)換為MMKV進(jìn)行存儲(chǔ),只不過(guò)需要注意一點(diǎn)容贝,不可回退自脯。
且聽(tīng)我慢慢道來(lái)
SP具體存在哪些問(wèn)題
- 容易anr,無(wú)論是commit斤富、apply膏潮、getxxx都可能導(dǎo)致ANR。
SharedPreferences 本身是一個(gè)接口满力,其具體的實(shí)現(xiàn)類是 SharedPreferencesImpl戏罢,而 Context 的各個(gè)和 SharedPreferences 相關(guān)的方法則是由 ContextImpl 來(lái)實(shí)現(xiàn)的屋谭。而每當(dāng)我們獲取到一個(gè) SharedPreferences 對(duì)象時(shí),這個(gè)對(duì)象將一直被保存在內(nèi)存當(dāng)中龟糕,如果SP文件過(guò)大,那么會(huì)對(duì)內(nèi)存的占用是有很大的影響的悔耘。
如果SP文件過(guò)大的話讲岁,在App啟動(dòng)的時(shí)候也會(huì)造成啟動(dòng)慢,甚至ANR的衬以。
class ContextImpl extends Context {
//根據(jù)應(yīng)用包名緩存所有 SharedPreferences缓艳,根據(jù) xmlFile 和具體的 SharedPreferencesImpl 對(duì)應(yīng)上
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
//根據(jù) fileName 拿到對(duì)應(yīng)的 xmlFile
private ArrayMap<String, File> mSharedPrefsPaths;
}
如果我們?cè)诔跏蓟?SharedPreferencesImpl 后緊接著就去 getValue 的話,勢(shì)必也需要確保子線程已經(jīng)加載完成后才去進(jìn)行取值操作看峻。SharedPreferencesImpl 就通過(guò)在每個(gè) getValue 方法中調(diào)用 awaitLoadedLocked()方法來(lái)判斷是否需要阻塞外部線程阶淘,確保取值操作一定會(huì)在子線程執(zhí)行完畢后才執(zhí)行。loadFromDisk()方法會(huì)在任務(wù)執(zhí)行完畢后調(diào)用 mLock.notifyAll()喚醒所有被阻塞的線程互妓。所以說(shuō)溪窒,如果 SharedPreferences 存儲(chǔ)的數(shù)據(jù)量很大的話,那么就有可能導(dǎo)致外部的調(diào)用者線程被阻塞冯勉,嚴(yán)重時(shí)甚至可能導(dǎo)致 ANR澈蚌。當(dāng)然,這種可能性也只是發(fā)生在加載磁盤(pán)文件完成之前灼狰,當(dāng)加載完成后 awaitLoadedLocked()方法自然不會(huì)阻塞線程宛瞄。這也是為什么第一次寫(xiě)入或者讀取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來(lái)說(shuō),性能較弱蝴簇。
之前也是做過(guò)ProtoBuffer的原理杯活,首先我們知道ProtoBuffer體積非常小,所以在存儲(chǔ)上就占據(jù)了很大的優(yōu)勢(shì)军熏。MMKV底層序列化和反序列化是ProtoBuffer實(shí)現(xiàn)的轩猩,所以在存儲(chǔ)速度上也有著很大的優(yōu)勢(shì)。 - 每次寫(xiě)入數(shù)據(jù)的時(shí)候是全量寫(xiě)入荡澎。假如xml有100條數(shù)據(jù)均践,當(dāng)插入一條新的數(shù)據(jù)或者更新一條數(shù)據(jù),SP會(huì)將全部的數(shù)據(jù)全部重新寫(xiě)入文件摩幔,這是造成SP寫(xiě)入慢的原因彤委。
- 當(dāng)保存的數(shù)據(jù)較多時(shí),會(huì)在進(jìn)程中占用過(guò)多的內(nèi)存或衡。
commit() 和 apply() 兩個(gè)方法都會(huì)通過(guò)調(diào)用 commitToMemory() 方法拿到修改后的全量數(shù)據(jù)commitToMemory()焦影,SharedPreferences 包含的所有鍵值對(duì)數(shù)據(jù)都存儲(chǔ)在 mapToWriteToDisk 中车遂,Editor 改動(dòng)到的所有鍵值對(duì)數(shù)據(jù)都存儲(chǔ)在 mModified 中。如果 mClear 為 true斯辰,則會(huì)先清空 mapToWriteToDisk舶担,然后再遍歷 mModified,將 mModified 中的所有改動(dòng)都同步給 mapToWriteToDisk彬呻。最終 mapToWriteToDisk 就保存了要重新寫(xiě)入到磁盤(pán)文件中的全量數(shù)據(jù)衣陶,SharedPreferences 會(huì)根據(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) {
//用于標(biāo)記最終是否改動(dòng)到了 mapToWriteToDisk
boolean changesMade = false;
if (mClear) {
if (!mapToWriteToDisk.isEmpty()) {
changesMade = true;
//清空所有在內(nèi)存中的數(shù)據(jù)
mapToWriteToDisk.clear();
}
keysCleared = true;
//恢復(fù)狀態(tài)闸氮,避免二次修改時(shí)狀態(tài)錯(cuò)位
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) { //意味著要移除該鍵值對(duì)
if (!mapToWriteToDisk.containsKey(k)) {
continue;
}
mapToWriteToDisk.remove(k);
} else { //對(duì)應(yīng)修改鍵值對(duì)值的情況
if (mapToWriteToDisk.containsKey(k)) {
Object existingValue = mapToWriteToDisk.get(k);
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
//只有在的確是修改了或新插入鍵值對(duì)的情況才需要保存值
mapToWriteToDisk.put(k, v);
}
changesMade = true;
if (hasListeners) {
keysModified.add(k);
}
}
//恢復(fù)狀態(tài)剪况,避免二次修改時(shí)狀態(tài)錯(cuò)位
mModified.clear();
if (changesMade) {
mCurrentMemoryStateGeneration++;
}
memoryStateGeneration = mCurrentMemoryStateGeneration;
}
}
return new MemoryCommitResult(memoryStateGeneration, keysCleared, keysModified,
listeners, mapToWriteToDisk);
}
- 不支持多進(jìn)程模式,想實(shí)現(xiàn)需要配合跨進(jìn)程通訊蒲跨。
如果想要實(shí)現(xiàn)多進(jìn)程共享數(shù)據(jù)译断,就需要自己去實(shí)現(xiàn)跨進(jìn)程通訊,比如ContentProvider或悲、AIDL孙咪、或者自己直接實(shí)現(xiàn)Binder等方式。
MMKV的優(yōu)點(diǎn)
- MMKV實(shí)現(xiàn)了SharedPreferences接口隆箩,基本可以無(wú)縫切換该贾。
MMKV提供了API可以直接將SP存儲(chǔ)的內(nèi)容直接轉(zhuǎn)向MMKV存儲(chǔ),不可回退捌臊。
SharedPreferences sources = context.getSharedPreferences(name, mode);
mmkv.importFromSharedPreferences(sources);
- 通過(guò)mmap映射文件杨蛋,通過(guò)一次拷貝。
通過(guò) mmap 內(nèi)存映射文件理澎,提供一段可供隨時(shí)寫(xiě)入的內(nèi)存塊逞力,App 只管往里面寫(xiě)數(shù)據(jù),由操作系統(tǒng)負(fù)責(zé)將內(nèi)存回寫(xiě)到文件糠爬,不必?fù)?dān)心 crash 導(dǎo)致數(shù)據(jù)丟失寇荧。通過(guò)內(nèi)存映射實(shí)現(xiàn)了文件到用戶空間只需要一次拷貝,而SP則需要兩次拷貝执隧。
mmap 是 linux 提供的一種內(nèi)存映射文件的方法揩抡,即將一個(gè)文件或者其他對(duì)象映射到進(jìn)程的地址空間,實(shí)現(xiàn)文件磁盤(pán)地址和進(jìn)程虛擬地址空間中一段虛擬地址的一一對(duì)應(yīng)關(guān)系镀琉;實(shí)現(xiàn)這樣的映射關(guān)系后峦嗤,進(jìn)程就可以采用指針的方式讀寫(xiě)操作這一塊內(nèi)存,而系統(tǒng)會(huì)自動(dòng)回寫(xiě)臟頁(yè)面到對(duì)應(yīng)的文件磁盤(pán)上屋摔,即完成了對(duì)文件的操作而不必調(diào)用read烁设,write等系統(tǒng)調(diào)用函數(shù)。
Binder 的底層也是通過(guò)了 mmap 來(lái)實(shí)現(xiàn)一次內(nèi)存拷貝的多進(jìn)程通訊钓试,所以MMKV也不用擔(dān)心多進(jìn)程下的數(shù)據(jù)持久化装黑。 - MMKV數(shù)據(jù)存儲(chǔ)序列化方面選用 protobuf 協(xié)議副瀑。
該協(xié)議類比xml有如下幾個(gè)有點(diǎn):- 語(yǔ)言無(wú)關(guān)、平臺(tái)無(wú)關(guān)恋谭。即 ProtoBuf 支持 Java糠睡、C++、Python 等多種語(yǔ)言箕别,支持多個(gè)平臺(tái)
- 高效铜幽。即比 XML 更小(3 ~ 10倍)串稀、更快(20 ~ 100倍)、更為簡(jiǎn)單
- 擴(kuò)展性狮杨、兼容性好母截。你可以更新數(shù)據(jù)結(jié)構(gòu),而不影響和破壞原有的舊程序
- MMKV是增量更新橄教,有性能優(yōu)勢(shì)清寇。
增量 kv 對(duì)象序列化后,直接 append 到內(nèi)存末尾护蝶;這樣同一個(gè) key 會(huì)有新舊若干份數(shù)據(jù)华烟,最新的數(shù)據(jù)在最后;那么只需在程序啟動(dòng)第一次打開(kāi) mmkv 時(shí)持灰,不斷用后讀入的 value 替換之前的值盔夜,就可以保證數(shù)據(jù)是最新有效的。用 append 實(shí)現(xiàn)增量更新帶來(lái)了一個(gè)新的問(wèn)題堤魁,就是不斷 append 的話喂链,文件大小會(huì)增長(zhǎng)得不可控。例如同一個(gè) key 不斷更新的話妥泉,是可能耗盡幾百 M 甚至上 G 空間椭微,而事實(shí)上整個(gè) kv 文件就這一個(gè) key,不到 1k 空間就存得下盲链。這明顯是不可取的蝇率。所以需要在性能和空間上做個(gè)折中:以內(nèi)存 pagesize 為單位申請(qǐng)空間,在空間用盡之前都是 append 模式刽沾;當(dāng) append 到文件末尾時(shí)本慕,進(jìn)行文件重整、key 排重悠轩,嘗試序列化保存排重結(jié)果间狂;排重后空間還是不夠用的話,將文件擴(kuò)大一倍火架,直到空間足夠鉴象。
MMKV的缺點(diǎn)
- 由上可知忙菠,Linux 采用了分頁(yè)來(lái)管理內(nèi)存,存入數(shù)據(jù)先要?jiǎng)?chuàng)建一個(gè)文件纺弊,并要給這個(gè)文件分配一個(gè)固定的大小牛欢。如果存入了一個(gè)很小的數(shù)據(jù),那么這個(gè)文件其余的內(nèi)存就會(huì)被浪費(fèi)淆游。相反如果存入的數(shù)據(jù)比文件大傍睹,就需要?jiǎng)討B(tài)擴(kuò)容。
- 還有一點(diǎn)就是 SP 轉(zhuǎn) MMKV 簡(jiǎn)單犹菱,如果想要再將 MMKV 轉(zhuǎn)換為其它方式的話拾稳,現(xiàn)在是不支持的。如果哪一天 Jetpack DataStore 崛起了腊脱,遷移起來(lái)可能會(huì)比較麻煩访得。
如何替換并且兼容
如何替換才能更好的兼容之前的代碼呢?直接上代碼陕凹,代碼很簡(jiǎn)單悍抑,一看就懂。
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ù)寫(xiě)入MMKV杜耙,為false搜骡,表示數(shù)據(jù)從MMKV寫(xiě)入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)異常拋埋點(diǎn)給服務(wù)端
MyStatistics.getEvent().eventNormal("MMKV", 0, 102, name);
return;
}
}
} catch (Throwable ex) {
//如果出現(xiàn)異常拋埋點(diǎn)給服務(wù)端
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;
}
/**
* 無(wú)實(shí)際意義,只是為了適配以前已經(jīng)調(diào)用了commit的舊的方式
*/
public boolean commit() {
if (!mMMKVEnabled) {
if (null != spEditor) {
return spEditor.commit();
}
}
return true;
}
/**
* 無(wú)實(shí)際意義佑女,只是為了適配以前已經(jīng)調(diào)用了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;
}
}
寫(xiě)到最后
最后记靡,最重要的就是MMKV的缺點(diǎn),遷移到MMKV是不可逆操作珊豹,一定要慎重簸呈。