錦囊篇|一文摸懂SharedPreferences和MMKV(一)

目錄

使用方法

SharedPreferences

// 1:得到SharedPreferences,這是直接包含在Context中的方式,直接調用便可
// 四種寫入模式:MODE_PRIVATE、MODE_APPEND、MODE_WORLD_READABLE、MODE_WORLD_WRITEABLE
val sp = baseContext.getSharedPreferences("clericyi", Context.MODE_PRIVATE)
// 2:獲取筆,由於第一步得到到至關於一張白紙,須要對應的筆才能對其操做
val editor = sp.edit()
// 3:數據操做,不過咱們當前操做的數據只是一個副本
// putString()、putInt()。。。還有不少方法
editor.putBoolean("is_wirte", true)
// 4:兩種提交方式,將副本內的數據正式寫入實體文件中
editor.commit() // 同步寫入
editor.apply() // 異步寫入

MMKV

第一步:開源庫導入java

implementation 'com.tencent:mmkv-static:1.1.2'

第二步:使用android

// 1. 自定義Aapplication
public void onCreate() {
    super.onCreate();
    MMKV.initialize(this);
}

// 2. 調度使用
// 和SharedPreferenced同樣,支持的數據類型直接往裏面塞便可
// 不同的地方,MMKV不須要本身去作一些apply()或者是commit()的操做,更加方便
MMKV kv = MMKV.defaultMMKV();

kv.encode("bool", true);
boolean bValue = kv.decodeBool("bool");

爲何這篇文章要拿兩個框架講?

單進程性能安全

多進程性能多線程

不管是單線程仍是多線程,MMKV的讀寫能力都遠遠的甩開了SharedPreferences&SQLite&SQLite+Transacion,可是MMKV究竟是如何作到如此快的進行讀寫操做的?這就是下面會經過源碼分析完成的事情了。併發

另外接下來的一句話僅表明了個人我的意見,也是爲何我只寫SharedPreferencesMMKV二者比較的緣由,由於我我的認爲SQLite和他們不太屬於同一類產品,因此比較的意義上來講就趨於普通。app

SharedPreferences源碼分析

根據上述中所說起過的使用代碼,可以比較清楚的知道第一步的分析對象就是getSharedPreferences()的獲取操做了,可是若是你直接點進去搜這個方法,是否是會出現這樣的結果呢?框架

public abstract SharedPreferences getSharedPreferences(String name, @PreferencesMode int mode);

沒錯了,只是一個抽象方法,那顯然如今最重要的事情就是找到他的具體實現類是什麼了,固然你能夠直接查閱資料獲取,最後的正確答案就是ContextImpl,不知道你有沒有找對呢?異步

public SharedPreferences getSharedPreferences(File file, int mode) {
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
            final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
            sp = cache.get(file);
            if (sp == null) {
                // .....
                // 經過具體實現類,對SharedPreferences進行建立
                sp = new SharedPreferencesImpl(file, mode);
                // 經過一個cache來防止同一個文件的SharedPreferences的重複建立
                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;
    }

在上面的使用過程當中提到了,其餘他是一個副本的概念,這個從何提及呢?顯然這就要看一下SharedPreferences的實現類具體是如何進行操做的了,從他的構造函數看起,慢慢進入深度調用。ide

SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        mBackupFile = makeBackupFile(file);
        mMode = mode;
        mLoaded = false;
        mMap = null;
        mThrowable = null;
        // 開始從磁盤中調數據
        startLoadFromDisk(); // 1 --> 
    }
// 1 -->
private void startLoadFromDisk() {
        // 開啓一條新的線程來加載數據
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                loadFromDisk(); // 2-->
            }
        }.start();
    }
// 2 -->
private void loadFromDisk() {
        Map<String, Object> map = null;
        StructStat stat = null;
        Throwable thrown = null;
        // 從XML中把數據讀出來,並把數據轉化成Map類型
        // 這是一個很是消耗時間的操做
        stat = Os.stat(mFile.getPath());
        if (mFile.canRead()) {
            BufferedInputStream str = null;
            str = new BufferedInputStream(
                    new FileInputStream(mFile), 16 * 1024);
            map = (Map<String, Object>) XmlUtils.readMapXml(str);
        }

        synchronized (mLock) {
            mLoaded = true;
            mThrowable = thrown;
            if (thrown == null) {
                // 文件裏拿到的數據爲空就重建,存在就賦值
                if (map != null) {
                    // 將數據存儲放置到具體類的一個全局變量中
                    // 稍微記一下這個關鍵點
                    mMap = map; 
                    mStatTimestamp = stat.st_mtim;
                    mStatSize = stat.st_size;
                } else {
                    mMap = new HashMap<>();
                }
            }
        }
    }

到此爲止就基本完成了SharedPreferences的構建流程,而爲了可以對數據進行操做,那就須要去獲取一隻筆,來進行操做,一樣的這段代碼最後會在SharedPreferencesImpl中進行具體實現。函數

public Editor edit() {
        synchronized (mLock) {
            awaitLoadedLocked();
        }
        // 很簡單的一個操做,就是建立了一支筆
        // 這裏是一個很重要的點,由於每次都是新建立一支筆,因此要作到數據更換的操做要一次性完成。
        return new EditorImpl();
    }

由於後面的操做都是與這隻筆相關,並且具體操做上重複度比較高,因此只選取一個putString()來進行分析。

public final class EditorImpl implements Editor {
    private final Map<String, Object> mModified = new HashMap<>();
    
    public Editor putString(String key, @Nullable String value) {
            synchronized (mEditorLock) {
                mModified.put(key, value);
                return this;
            }
        }
}

很簡單的解決思路,就是新建立一個了HashMap裏面所有保存的都是一些咱們已經作過修改的數據,以後的更新是須要用到這些數據的。

相較於以前的那些源碼,這裏的就顯得很是輕鬆了,結合上述的源碼分析,能夠假設SharedPreferences氛圍三個要點。

  1. mMap: 存儲從文件中拉取的數據。
  2. mModified: 存儲但願修改值的數據。
  3. apply()/commit(): 猜想最後就是上述二者數據的合併,再進行數據提交。

數據提交

異步提交 / apply()

public void apply() {
            // .....
            // 這一步其實就是咱們所猜想的第三步中的數據合併
            // 作一個簡單的介紹,數據的替換一共分爲三步:
            // 1. 將數據存儲到mapToWriteToDisk中
            // 2. 與mModified中數據進行比較,不存在或者不一致就替換
            // 3. 將更新後得數據返回
            final MemoryCommitResult mcr = commitToMemory();
            // 經過CountDownLatch來完成數據的同步更新
            final Runnable awaitCommit = new Runnable() {
                    @Override
                    public void run() {
                        mcr.writtenToDiskLatch.await(); // 1-->
                    }
                };
            // 對事件完成的監聽
            QueuedWork.addFinisher(awaitCommit);

            Runnable postWriteRunnable = new Runnable() {
                    @Override
                    public void run() {
                        // 經過線程進行異步處理
                        awaitCommit.run();
                        // 若是任務完成,就從隊列中清除
                        QueuedWork.removeFinisher(awaitCommit);
                    }
                };
            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);  // 2 -->
            // 通知觀察者數據更新
            notifyListeners(mcr);
        }

從上述的代碼中能夠了解到apply()的經過建立一個線程來進行處理,以後會講到commit()和他的處理方式不一樣的地方。如今具體的目光仍是要聚焦在如何完成數據到磁盤的提交的,也就是註釋1處的具體實現究竟是如何?這就是對這個類的一個理解問題了。其實他有點相似於程序計數器,在阻塞數量大於線程數時,會阻塞運行,而超出數量就會出現併發情況。

第二個地方就是註釋2,他線程作了一個入隊列的操做。

private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
        final boolean isFromSyncCommit = (postWriteRunnable == null);

        final Runnable writeToDiskRunnable = new Runnable() {
                @Override
                public void run() {
                    synchronized (mWritingToDiskLock) {
                        writeToFile(mcr, isFromSyncCommit);
                    }
                }
            };
        // commit()一樣會進入到這個大方法中
        // commit()方法執行到這裏運行完就結束,乾的事情就是將數據寫入文件
        if (isFromSyncCommit) {
            writeToDiskRunnable.run();
            return;
        }
        // apply()多作了層如隊列的操做,意圖在於異步進行
        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit); // 3-->
    }
// 3 -->
// 由於最後使用的都是其實都是MSG_RUN的參數,因此直接調用查看便可
public static void queue(Runnable work, boolean shouldDelay) {
        Handler handler = getHandler();

        synchronized (sLock) {
            sWork.add(work);

            if (shouldDelay && sCanDelay) {
                handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY); // 4-->
            } else {
                handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN); // 4-->
            }
        }
    }
// 4 -->
public void handleMessage(Message msg) {
    if (msg.what == MSG_RUN) {
        processPendingWork(); // 5 -->
    }
}
// 5 -->
// 就是最後將一個個任務進行完成運行
private static void processPendingWork() {
        synchronized (sProcessingWork) {
            LinkedList<Runnable> work;
            // 。。。。。
            if (work.size() > 0) {
                for (Runnable w : work) {
                    w.run(); // 最後將數據一個個進行運行完成操做
                }
            }
        }
    }

同步提交 / commit()

public boolean commit() {
            
            MemoryCommitResult mcr = commitToMemory();
            // 不須要使用線程來進行異步處理,因此第二參數爲空
            SharedPreferencesImpl.this.enqueueDiskWrite(
                mcr, null /* sync write on this thread okay */);
                
            mcr.writtenToDiskLatch.await();
            
            notifyListeners(mcr);
            return mcr.writeToDiskResult;
        }

因此說基本邏輯上其實仍是和apply()方法是一致的,只是去除了異步處理的步驟,因此就是常說的同步處理方式。

總結

  1. 是什麼限制了SharedPreferences的處理速度?

這個問題在上面的源碼分析中其實已經有所說起了,那就是文件讀寫,因此如何加快文件的讀寫速度是一個相當重要的突破點。固然向速度妥協的一個方案,想來你也已經看到了,那就是異步提交,經過子線程的在用戶無感知的狀況下把數據寫到文件中。

  1. 爲何多線程安全,而多進程不安全的操做?

多線程安全安全想來是一個很是容易解釋的事情了,幹一個很簡單的事情就是synchronized的加鎖操做,對數據的操做進行加鎖那勢必拿到的最後數據就會是一個安全的數據了。

可是對於多進程呢? 你可能會說在sp.startReloadIfChangedUnexpectedly();這段代碼出現的難道不是已經涉及了多進程的安全操做嗎?yep!! 若是你想到了這點,說明你有好好看了下代碼,可是沒有看他的實現,若是你去看他的實現方案,就會發現MODE_MULTI_PROCESS和所可使用的操做的運算結果均爲0,因此在如今的Android版本中這是一個被拋棄的方案。固然這是其一,天然還有另一個判斷就是關於版本方面,若是小於HONEYCOMB一樣能夠進入這個方案,可是須要注意getSharedPreferences()是隻有獲取時纔會出現的,而SharedPreferences是對於單進程而言的單獨實例,數據的備份所有在單個進程完成,因此在進行多進程讀寫時,發生錯誤是大機率的。

相關文章
相關標籤/搜索