Android SharedPreferences轉(zhuǎn)為MMKV

開(kāi)篇廢話

開(kāi)局一張圖姻政,說(shuō)明一切問(wèn)題但汞。


MMKV優(yōu)勢(shì)

可以看出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是不可逆操作珊豹,一定要慎重簸呈。

更多內(nèi)容戳這里(整理好的各種文集)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市店茶,隨后出現(xiàn)的幾起案子蜕便,更是在濱河造成了極大的恐慌,老刑警劉巖贩幻,帶你破解...
    沈念sama閱讀 216,997評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件轿腺,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡丛楚,警方通過(guò)查閱死者的電腦和手機(jī)族壳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,603評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)趣些,“玉大人仿荆,你說(shuō)我怎么就攤上這事。” “怎么了拢操?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,359評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵锦亦,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我令境,道長(zhǎng)杠园,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,309評(píng)論 1 292
  • 正文 為了忘掉前任舔庶,我火速辦了婚禮抛蚁,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘惕橙。我一直安慰自己瞧甩,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,346評(píng)論 6 390
  • 文/花漫 我一把揭開(kāi)白布弥鹦。 她就那樣靜靜地躺著亲配,像睡著了一般。 火紅的嫁衣襯著肌膚如雪惶凝。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,258評(píng)論 1 300
  • 那天犬钢,我揣著相機(jī)與錄音苍鲜,去河邊找鬼。 笑死玷犹,一個(gè)胖子當(dāng)著我的面吹牛混滔,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播歹颓,決...
    沈念sama閱讀 40,122評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼坯屿,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了巍扛?” 一聲冷哼從身側(cè)響起领跛,我...
    開(kāi)封第一講書(shū)人閱讀 38,970評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎撤奸,沒(méi)想到半個(gè)月后吠昭,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,403評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡胧瓜,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,596評(píng)論 3 334
  • 正文 我和宋清朗相戀三年矢棚,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片府喳。...
    茶點(diǎn)故事閱讀 39,769評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蒲肋,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情兜粘,我是刑警寧澤申窘,帶...
    沈念sama閱讀 35,464評(píng)論 5 344
  • 正文 年R本政府宣布,位于F島的核電站妹沙,受9級(jí)特大地震影響偶洋,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜距糖,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,075評(píng)論 3 327
  • 文/蒙蒙 一玄窝、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧悍引,春花似錦恩脂、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,705評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至浓领,卻和暖如春玉凯,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背联贩。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,848評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工漫仆, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留烙如,地道東北人死陆。 一個(gè)月前我還...
    沈念sama閱讀 47,831評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像臭脓,于是被迫代替她去往敵國(guó)和親祸泪。 傳聞我的和親對(duì)象是個(gè)殘疾皇子吗浩,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,678評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容