Android SharedPreferences轉為MMKV

開篇廢話

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


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是不可逆操作,一定要慎重。

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

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容