SharedPreferences靈魂拷問之原理

先來一波靈魂追問:java

  • 據說提交要用apply(),爲何?
  • 和commit()什麼區別?
  • 跨進程怎麼操做?
  • 會堵塞主線程嗎?
  • 很着急有替代方案嗎?

( 年底福利: 知道你很忙,參考答案可直接看文末... )git

一、加載/初始化

image

一切從getSharedPreference(String name,int Mode)這個方法提及;經過這個方法獲取到一個SharedPreference實例。SharedPreferences是一個接口(interface),他的具體實現類爲SharedPreferencesImpl。 SharedPreference的加載的主要過程:github

  • 找到對應name的文件。
  • 加載對應文件到內存中SharedPreference。
  • 一個xml文件對應一個ShredPreferences單例
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
private ArrayMap<String, File> mSharedPrefsPaths;
複製代碼

sSharedPrefsCache存儲的是File和SharedPreferencesImpl鍵值對,當對應File的SharedPreferencesImpl加載以後就會一支存儲於sSharedPrefsCache中。相似的mSharedPrefsPaths存儲的是name和File的對應關係。使用的ArrayMap,關於ArrayMap這種Android特有的數據結構,詳細瞭解能夠看這juejin.im/post/5d550f…微信

當經過name最終找到對應的File以後,就會實例化一個SharedPreferencesImpl對象。在SharedPreferences構造方法中開啓一個子線程加載磁盤中的xml文件。數據結構

你們都應該很明確的一點是,SP持久化的本質是在本地磁盤記錄了一個xml文件,這個文件所在的文件夾shared_prefsapp

image

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

怎麼保證使用sp.get(String name)的時候SP的初始化或者說從磁盤中加載到內存中這一過程已經完成了呢?異步

public String getString(String key, @Nullable String defValue) {
        synchronized (mLock) {
            awaitLoadedLocked();
            String v = (String)mMap.get(key);
            return v != null ? v : defValue;
        }
    }

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);
        }
    }
複製代碼

使用awaitLoadedLocked()方法檢測,是否已經加載完成,若是沒有加載完成,就等待堵塞。等加載完成以後,繼續執行;ide

在loadFromDisk()方法中,若是加載成功會把mLoaded標誌位置爲true,而後 mLock.notifyAll();post

最終,就把位於磁盤中的文件,加載到了內存中對應一個SharedPreferces對象,SharedPreferences中mMap。ui

二、編輯提交

當想SP中存入數據的時候,實例代碼以下。

sharedPreferences.edit().putInt("number", 100).puString("age","18").apply();
sharedPreferences.edit().putInt("number", 100).commit();
複製代碼

調用sharedPreferences.edit()返回一個EditorImpl對象,操做數據以後調用apply()或者commit()。

2.一、 commit()流程

@Override
    public boolean commit() {
    MemoryCommitResult mcr = commitToMemory();//寫入內存
    SharedPreferencesImpl.this.enqueueDiskWrite(//寫入磁盤
          mcr, null /* sync write on this thread okay */);
        try {
            mcr.writtenToDiskLatch.await();//等待寫入磁盤執行完畢
        } catch (InterruptedException e) {
                return false;
        } finally {}
        notifyListeners(mcr);//通知監聽
        return mcr.writeToDiskResult;
      }
      
     //
    private void enqueueDiskWrite(final MemoryCommitResult mcr,final Runnable postWriteRunnable) {
        //若是postWriteRunnable爲空表示來自commit()方法調用
        final boolean isFromSyncCommit = (postWriteRunnable == null);
        final Runnable writeToDiskRunnable = new Runnable() {
                @Override
                public void run() {
                    synchronized (mWritingToDiskLock) {
                        writeToFile(mcr, isFromSyncCommit);//寫入磁盤
                    }
                    synchronized (mLock) {
                        mDiskWritesInFlight--;
                    }
                    if (postWriteRunnable != null) {
                        postWriteRunnable.run();
                    }
                }
            };
            //當commit提交,且mDiskWritesInFlight爲1的時候,直接在當前所在線程執行寫入磁盤操做
        if (isFromSyncCommit) {
            boolean wasEmpty = false;
            synchronized (mLock) {
                wasEmpty = mDiskWritesInFlight == 1;
            }
            if (wasEmpty) {
                writeToDiskRunnable.run();
                return;
            }
        }
        
        //交個QueuedWork,QueuedWork內部維護了一個HandlerThread,一直執行寫入磁盤操做。
        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
    }

複製代碼

image

如註釋:當調用commit()方法以後

  • 首先將編輯的結果同步到內存中。

  • enqueueDiskWrite()將這個結果同步到磁盤中,enqueueDiskWrite()的第二個參數postWriteRunnable傳入空。一般狀況下也就是mDiskWritesInFlight(正在執行的寫入磁盤操做的數量)爲1的時候,直接在當前所在線程執行寫入磁盤操做。不然仍是異步到QueuedWork中去執行。commit()時,寫入磁盤操做會發生在當前線程的說法是不許確的

  • 執行mcr.writtenToDiskLatch.await(); MemoryCommitResult 中有個一個CountDownLatch 成員變量,他的具體做用能夠查閱其餘資料。總的來講,當前線程執行會堵塞在這,直到mcr.writtenToDiskLatch知足了條件。也就是當寫入磁盤成功以後,會繼續執行下面的操做。

  • 因此,commit提交以後會有返回結果,同步堵塞直到有返回結果

2.二、 apply()流程

@Override
        public void apply() {
            final long startTime = System.currentTimeMillis();
            final MemoryCommitResult mcr = commitToMemory();
            final Runnable awaitCommit = new Runnable() {
                    @Override
                    public void run() {mcr.writtenToDiskLatch.await();}
                   };
            QueuedWork.addFinisher(awaitCommit);
            Runnable postWriteRunnable = new Runnable() {
                    @Override
                    public void run() {
                        awaitCommit.run();
                        QueuedWork.removeFinisher(awaitCommit);
                    }
                };
            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
            notifyListeners(mcr);
        }
複製代碼
  • 加入到QueuedWork中,是一個單線程的操做。
  • 沒有返回結果。
  • 默認會有100ms的延遲

2.3 、QueuedWork

2.3.一、 關於延遲磁盤寫入。
/** Delay for delayed runnables, as big as possible but low enough to be barely perceivable */
    private static final long DELAY = 100;
    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);
            } else {
                handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
            }
        }
複製代碼
  • 當apply()方式提交的時候,默認消息會延遲發送100毫秒,避免頻繁的磁盤寫入操做。
  • 當commit()方式,調用QueuedWork的queue()時,會當即向handler()發送Message。
2.3.二、主線程堵塞ANR

You don't need to worry about Android component lifecycles and their interaction with apply() writing to disk. The framework makes sure in-flight disk writes from apply() complete before switching states.

官方文檔中有這樣段化話,意思是您不須要擔憂Android組件生命週期及其對apply()寫入磁盤的影響。框層架確保在切換狀態以前完成使用apply()方法正在執行磁盤寫入的動做。

然而還真是不讓人那麼省心。

罪魁禍首在這:

//QueuedWork.java
    public static void waitToFinish() {
        ...
          processPendingWork();//執行文件寫入磁盤操做
        ....
    }
    private static void processPendingWork() {
        long startTime = 0;
      ....
     if (work.size() > 0) {
         for (Runnable w : work) {
             w.run();
         }
      ...  
    }
複製代碼

waitToFinish()會將,儲存在QueuedWork的操做一併處理掉。何時呢?在Activiy的 onPause()、BroadcastReceiver的onReceive()以及Service的onStartCommand()方法以前都會調用waitToFinish()。你們知道這些方法都是執行在主線程中,一旦waitToFinish()執行超時,就會跑出ANR。

至於waitToFinish調用具體時機,查看ActivityThread.java類文件。這裏只是說本質原理。

三、跨進程操做的解決方案

\\ContextImpl private void checkMode(int mode) {
        if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.N) {
            if ((mode & MODE_WORLD_READABLE) != 0) {
                throw new SecurityException("MODE_WORLD_READABLE no longer supported");
            }
            if ((mode & MODE_WORLD_WRITEABLE) != 0) {
                throw new SecurityException("MODE_WORLD_WRITEABLE no longer supported");
            }
        }
    }
複製代碼

Andorid 7.0及以上會拋出異常,Sharepreferences再也不支持多進程模式。多進程共享文件會出現問題的本質在於,由於不一樣進程,因此線程同步會失效。要解決這個問題,可嘗試跨進程解決方案,如ContentProvider、AIDL、AIDL、Service。

四、替代方案

  • 有問題,主線程堵塞。
  • 效率低。
  • 一不留神容易產生ANR。

既然SharedPreferences有這麼多問題?就沒人管管嗎? 溫和的治理方法或者說小建議

4.一、 溫和改良派

  • 低頻 儘可能保證屢次edit一個apply,緣由上文講過,儘可能維持低頻的寫入。
  • 異步 能用apply()方法提交的就用apply()方法提交,緣由這個方法是異步的,有延遲的(100s)
  • 小量 儘可能維持Sharepreferences的體量小些,方便磁盤快速寫入。
  • 合規 若是村JSON數據,就不要使用Sharepreferences了,由於SharedPerences本質是xml文件格式存儲的,要存儲JSON文件須要轉義效率很低。不如直接本身編寫代碼文件讀寫在App私有目錄中存儲。

4.二、 激進剷除派

  • 騰訊微信團隊的MMKV採用內存映射的方式,解決SharedPreferences的各類問題。
  • 原理基於內存映射mmap,具體使用 原理 源碼 github.com/Tencent/MMK…

五、 小結

經過本文咱們瞭解了SharedPreferences的基本原理。再回頭看看文章開頭的那幾個問題,是否是有答案了。

  • commit()方法和apply()方法的區別:commit()方法是同步的有返回結果,同步保證使用Countdownlatch,即便同步但不保證往磁盤的寫入是發生在當前線程的。apply()方法是異步的具體發生在QueuedWork中,裏面維護了一個單線程去執行磁盤寫入操做。
  • commit()和apply()方法其實都是Block主線程。commit()只要在主線程調用就會堵塞主線程;apply()方法磁盤寫入操做雖然是異步的,可是當組件(Activity Service BroadCastReceiver)這些系統組件特定狀態轉換的時候,會把QueuedWork中未完成的那些磁盤寫入操做放在主線程執行,且若是比較耗時會產生ANR,手動可怕。
  • 跨進程操做,須要藉助Android平臺常規的IPC手段(如,AIDL ContentProvider等)來完成。
  • 替代解決方案:看4。
相關文章
相關標籤/搜索