大數據

SharedPreferences VS MMKV

  **SharedPreferences** 作為輕量級存儲在 **Android** 應用中是必不可少的,但依舊存在較大的優化空間,小菜在做性能優化時嘗試了新的利器 **騰訊 MMKV**,小菜今天按如下腦圖順序嘗試學習和簡單分析一下;

SharedPreferences

1. SharedPreferences 基本介紹

  **SharedPreferences** 是一種輕量級存儲方式,以 **key-value** 方式存儲在本地 **xml** 文件中;其持久化的本質就是在在本地磁盤記錄一個 **xml** 文件;
public interface SharedPreferences {
    public interface OnSharedPreferenceChangeListener { 
        void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key);
    }
    void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);
    void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);
    Editor edit();
    public interface Editor {
        Editor putString(String key, @Nullable String value);
        Editor putStringSet(String key, @Nullable Set<String> values);
        ...
        Editor remove(String key);
        Editor clear();
        boolean commit();
        void apply();
    }
}
  簡單分析源碼可得,**SharedPreferences** 只是一個接口,**SharedPreferencesImpl** 為具體的實現類,通過 **ContextImpl** 中 **getSharedPreferences()** 獲取對象;

2. SharedPreferences 初始化

SharedPreferences sp = getSharedPreferences(Constants.SP_APP_CONFIG, MODE_PRIVATE);
  **SharedPreferences** 的通過 **getSharedPreferences()** 初始化創建一個對象;其中 **MODE** 為文件操作類型;**MODE_PRIVATE** 為本應用私有的,其他 **app** 不可訪問的;**MODE_APPEND** 也為應用私有,但是新保存的數據放置在文件最後,不會替換之前已有的 **key-value**;**MODE_WORLD_READABLE/WRITEABLE** 為其他文件是否可以支持讀寫操作;常用的還是 **MODE_PRIVATE** 方式;

@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
    if (mPackageInfo.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.KITKAT) {
        if (name == null) { name = "null"; }
    }

    File file;
    synchronized (ContextImpl.class) {
        if (mSharedPrefsPaths == null) {
            // TAG 01
            mSharedPrefsPaths = new ArrayMap<>();
        }
        file = mSharedPrefsPaths.get(name);
        if (file == null) {
            file = getSharedPreferencesPath(name);
            mSharedPrefsPaths.put(name, file);
        }
    }
    return getSharedPreferences(file, mode);
}

@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
    SharedPreferencesImpl sp;
    synchronized (ContextImpl.class) {
        // TAG 02
        final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
        sp = cache.get(file);
        if (sp == null) {
            checkMode(mode);
            if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) {
                if (isCredentialProtectedStorage() && !getSystemService(UserManager.class).isUserUnlockingOrUnlocked(UserHandle.myUserId())) {
                    throw new IllegalStateException("SharedPreferences in credential encrypted storage are not available until after user is unlocked");
                }
            }
            // TAG 03
            sp = new SharedPreferencesImpl(file, mode);
            cache.put(file, sp);
            return sp;
        }
    }
    if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
        getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
        sp.startReloadIfChangedUnexpectedly();
    }
    return sp;
}
  小菜在源碼處註明了幾個 **TAG** 需要注意的地方;

TAG 01: 在根據 name 查詢文件時,SharedPreferences 使用了 ArrayMap,相較於 HashMap 更便捷,更節省空間;

TAG 02: 在創建生成 SharedPreferences 時,通過 cache 來防止同一個 SharedPreferences 被重複創建;

TAG 03: SharedPreferencesImapl 為具體的實現類,初始化時開啟新的 I/O 線程讀取整個文件 startLoadFromDisk(),進行 xml 解析,存入內存 Map 集合中;

SharedPreferencesImpl(File file, int mode) {
    mFile = file;       
    mBackupFile = makeBackupFile(file);
    mMode = mode;       
    mLoaded = false;
    mMap = null;        
    mThrowable = null;
    startLoadFromDisk();
}

private void startLoadFromDisk() {
    synchronized (mLock) { mLoaded = false; }
    new Thread("SharedPreferencesImpl-load") {
        public void run() { loadFromDisk(); }
    }.start();
}

3. SharedPreferences 編輯提交

// 編輯數據
Editor editor = sp.edit();
editor.putString("name", "阿策小和尚");
// 提交數據
editor.apply();

// 獲取數據
Editor editor = sp.edit();
editor.getString("name", "");
  **Editor** 是用於編輯 **SharedPreferences** 內容的接口,**EditorImpl** 為具體的實現類;**putXXX()** 編輯後的數據保存在 **Editor** 中,**commit()/apply()** 後才會更新到 **SharedPreferences**;
@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) {
        BlockGuard.getThreadPolicy().onReadFromDisk();
    }
    while (!mLoaded) {
        try {
            mLock.wait();
        } catch (InterruptedException unused) {
        }
    }
    if (mThrowable != null) {
        throw new IllegalStateException(mThrowable);
    }
}
  **getXXX()** 獲取數據時根據 **mLoaded** 文件是否讀取完成判斷,若未讀取完成 **awaitLoadedLocked()** 會被阻塞,此時在 **UI** 主線程中進行使用時就可有可能會造成 **ANR**;
@Override
public void apply() {
    final long startTime = System.currentTimeMillis();
    final MemoryCommitResult mcr = commitToMemory();
    final Runnable awaitCommit = new Runnable() {
            @Override
            public void run() {
                try {
                    mcr.writtenToDiskLatch.await();
                } catch (InterruptedException ignored) {
                }
            }
        };
    QueuedWork.addFinisher(awaitCommit);
    Runnable postWriteRunnable = new Runnable() {
            @Override
            public void run() {
                awaitCommit.run();
                QueuedWork.removeFinisher(awaitCommit);
            }
        };
    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
    notifyListeners(mcr);
}
  **Editor** 通過 **commit()** 和 **apply()** 提交更新到 **SharedPrefenences**;兩者的區別很明顯,**apply()** 通過線程進行異步處理,如果任務完成則從隊列中移除 **QueuedWork.removeFinisher**,無法獲取提交的結果;**commit** 是同步更新,使用時會阻塞主線程,因為是同步提交,可以獲取 **Boolean** 狀態的提交狀態,進而判斷是否提交成功;

4. SharedPreferences 問題與優化

  **SharedPreferences** 雖因其便利性而應用廣泛,但也存在一些弊端;
Q1: 編輯 get()/put() 時均會涉及到互斥鎖和寫入鎖,併發操作時影響性能;
A1: 讀寫操作都是針對的 SharedPreferences 對象,可適當拆分文件或降低訪問頻率等;
Q2: 使用時出現卡頓引發 GC 或 ANR;
A2:
  1. 不要存放大數據類型的 key-value 避免導致一直在內存中無法釋放;
  2. 儘量避免頻繁讀寫操作;
  3. 儘量減少 apply() 次數,每次都會新建一個 EditorImpl 對象,可以批量處理統一提交;
Q3: 不能跨進程通信,不能保證更新本地數據後被另一個進程所知;
A3: 可以藉助 ContentProvider 來在多進程中更新數據;

MMKV

1. MMKV 基本介紹

  正因為 **SharedPreferences** 還有很大的優化空間,因為我們才會嘗試其他存儲框架;其中 [**騰訊 MMKV**](https://github.com/Tencent/MMKV) 得到很多人的支持;

  **MMKV** 分別代表的是 **Memory Mapping Key Value**,是基於 **mmap** 內存映射的 **key-value** 組件,底層序列化/反序列化使用 **protobuf** 實現,性能高,穩定性強;官網 **Wiki** 介紹的優勢很明顯,是目前微信正在使用的輕量級存儲框架;在 **Android / macOS / Win32 / POSIX** 多個平臺一併開源;

2. MMKV 優勢

  小菜從如下幾個角度簡單分析一下 **MMKV** 的優勢;

a. 數據格式及更新範圍優化

  **SharedPreferences** 採用 **xml** 數據存儲,每次讀寫操作都會全局更新;**MMKV** 採用 **protobuf** 數據存儲,更緊密,支持局部更新

b. 文件耗時操作優化

  **MMKV** 採用 **MMap** 內存映射的方式取代 **I/O** 操作,使用 **0**拷貝技術提高更新速度;

c. 跨進程狀態同步

  **SharedPreferences** 為了線程安全不支持跨進程狀態同步;**MMKV** 通過 **CRC** 校驗 和文件鎖 **flock** 實現跨進程狀態更新;

d. 應用便捷性,較好的兼容性

  **MMKV** 使用方式便捷,與 **SharedPreferences** 基本一致,遷移成本低;

2.1 Memory Mapping 內存映射
  **Memory Mapping** 簡稱 **MMap** 是一種將磁盤上文件的一部分或整個文件映射到應用程序地址空間的一系列地址機制,從而應用程序可以用訪問內存的方式訪問磁盤文件;

  由此可見,**MMap** 的優勢很明顯了,因為進行了內存映射,操作內存相當於操作文件,無需開啟新的線程,相較於 **I/O** 對文件的讀寫操作只需要從磁盤到用戶主存的一次數據拷貝過程,減少了數據的拷貝次數,提高了文件的操作效率;同時 **MMap** 只需要提供一段內存,只需要關注往內存文件中讀寫操作即可,在操作系統內存不足或進程退出時自動寫入文件中;

  當然,**MMap** 也有自身的劣勢,因為 **MMap** 需要提供一度長度的內存塊,其映射區的長度默認是一頁,即 **4kb**,當存儲的文件內容較少時可能會造成空間的浪費;
2.2 Protocol Buffers 編碼結構
  **Protocol Buffers** 簡稱 **protobuf**,是 **Google** 出品的一種可擴展的序列化數據的編碼格式,主要用於通信協議和數據存儲等;利用 **varint** 原理(一種變長的編碼方式,值越小的數字,使用的字節越少)壓縮數據以後,二進制數據非常緊湊;

  **protobuf** 採用了 **TLV(TAG-Length-Value)** 的編碼格式,減少了分隔符的使用,編碼更為緊湊;

  **protobuf** 在更新文件時,雖然也不方便局部更新,但是可以做增量更新,即不管之前是否有相同的 **key**,一旦有新的數據便添加到文件最後,待最終文件讀取時,後面新的數據會覆蓋之前老舊的數據;

  當添加新的數據時文件大小不夠了,需要全量更新,此時需要將 **Map** 中數據按照 **MMKV** 方式序列化,濾重後保存需要的字節數,根據獲取的字節數與文件大小進行比較;若保存後的文件大小可以添加新的數據時直接添加在最後面,若保存後的文件大小還是不足以添加新的數據時,此時需要對 **protobuf * 2** 擴容;

  **protobuf** 功能簡單,作為二進制存儲,可讀性較差;同時無法表示複雜的概念,通用性相較於 **xml** 較差;這也是 **protobuf** 的不足之處;
2.3 flock 文件鎖 + CRC 校驗
  **SharedPreferences** 因為線程安全不支持在多進程中進行數據更新;而 **MMKV** 通過 **flock** 文件鎖和 **CRC** 校驗支持多進程的讀寫操作;

  小菜簡單理解,**MMKV** 在進程 **A** 中更新了數據,在進程 **B** 中獲取當前數據時會先通過 **CRC** 文件校驗看文件是否有過更新,若沒更新直接讀取,若已更新則重新獲取文件內容在進行讀取;

  而為了防止多個進程同時對文件進行寫操作,**MMKV** 採用了文件鎖 **flock** 方式來保證同一時間只有一個進程對文件進行寫操作;

3. MMKV 應用與注意

  **MMKV** 的應用非常簡單,根據官網集成即可:
  1. Maven 倉庫引入 mmkv
implementation 'com.tencent:mmkv-static:1.2.2'
  1. 初始化;
MMKV.initialize(this);
  1. 根據文件名稱創建對應存儲文件;建議設置 MMKV 為全局實例,方便統一處理;
// 默認文件名
MMKV kv = MMKV.defaultMMKV();

// 指定文件名
MMKV kv = MMKV.mmkvWithID(Constants.SP_APP_CONFIG);
  1. 可以通過 encode() 方式存儲數據也可以使用和 SharedPreferences 相同的 put() 方式存儲數據;
kv.encode("name", "阿策小和尚");
kv.encode("age", 18);

kv.putString("address", "北京市海淀區");
kv.putInt("sex", 0);
  1. 同樣可以採用 decodeXXX()getXXX() 獲取數據;
kv.decodeString("name", "");
kv.decodeInt("age", -1);

kv.getString("address", "");
kv.getInt("sex", -1);
  1. SharedPreferences 一樣,remove() 清除一條數據,clear() 清空全部數據;
kv.remove();

kv.clear();
  1. 對於應用中已存在 SharedPreferences 時,MMKV 提供了一鍵轉換為 MMKV 方式;
MMKV mmkv = MMKV.mmkvWithID(Constants.SP_APP_CONFIG);
SharedPreferences sp = context.getSharedPreferences(mid, Context.MODE_PRIVATE);
mmkv.importFromSharedPreferences(sp);
sp.edit().clear().commit();


  小菜對於 **SharedPreferences** 和 **MMKV** 的底層源碼還不夠深入,如有錯誤,請多多指導!

來源: 阿策小和尚

Leave a Reply

Your email address will not be published. Required fields are marked *