SharePreferences源碼解讀

細推物理須行樂,何用浮名絆此身java

前言

SharedPreferences(簡稱SP)是Android輕量級的鍵值對存儲方式。對於開發者來講它的使用很是的方便,可是也是一種被你們詬病不少的一種存儲方式。有所謂的七宗罪:android

  1. SP進程不安全,即便使用MODE_MULTI_PROCESS
  2. 全量寫入
  3. 加載緩慢
  4. 卡頓,apply異步落盤致使的anr

帶着這些結論咱們一步步的從代碼中找出它的依據,固然了,本文的內容不止如此,還包裹整個SharedPreferences的運行機理等,固然這一切都是我我的的理解,中間難免有錯誤的地方,也歡迎你們指證。c++

2. SharedPreferences實例的獲取

SharedPreferences的建立能夠有多種方式:git

2.1 ContextWrapper中獲取

# ContextWrapper.java
public SharedPreferences getSharedPreferences(String name, int mode) {
       return mBase.getSharedPreferences(name, mode);
   }

複製代碼

由於咱們的Activity,Service,Application都會繼承ContextWrapper,因此它們也能夠獲取到SharedPreferencesgithub

2.2 PreferenceManager中獲取

# PreferenceManager.java

public static SharedPreferences getDefaultSharedPreferences(Context context) {
      return context.getSharedPreferences(getDefaultSharedPreferencesName(context),
              getDefaultSharedPreferencesMode());
  }

複製代碼

經過PreferenceManager中靜態方法獲取,固然根據需求不通,PreferenceManager中還提供了別的方法,你們能夠去查閱。緩存

2.3. ContextImpl中獲取並建立SharedPreferences

雖然上面獲取SharedPreferences的方式不少,可是他們最終都會調用到ContextImpl.getSharedPreferences的方法,而且 SharedPreferences真正的建立也是在這裏,關於ContextImpl和Activity、Service等的關係,我會另外寫篇文章介紹,其實使用的是裝飾器模式.安全

2.3.1 getSharedPreferences(String name, int mode)

# ContextImpl.java

public SharedPreferences getSharedPreferences(String name, int mode) {
       // At least one application in the world actually passes in a null
       // name.  This happened to work because when we generated the file name
       // we would stringify it to "null.xml".  Nice.
       if (mPackageInfo.getApplicationInfo().targetSdkVersion <
               Build.VERSION_CODES.KITKAT) {
           if (name == null) {
               name = "null";
           }
       }

       File file;
       synchronized (ContextImpl.class) {
           if (mSharedPrefsPaths == null) {
               mSharedPrefsPaths = new ArrayMap<>();
           }
           //從mSharedPrefsPaths緩存中查詢文件
           file = mSharedPrefsPaths.get(name);
           if (file == null) {
                //若是文件不存在,根據name建立 [見2.3.2]
               file = getSharedPreferencesPath(name);
               mSharedPrefsPaths.put(name, file);
           }
       }
       //[見2.3.3]
       return getSharedPreferences(file, mode);
   }

複製代碼

2.3.2 getSharedPreferencesPath(name)

# ContextImpl.java
@Override
   public File getSharedPreferencesPath(String name) {
       return makeFilename(getPreferencesDir(), name + ".xml");
   }

   //建立目錄/data/data/package name/shared_prefs/
   private File getPreferencesDir() {
       synchronized (mSync) {
           if (mPreferencesDir == null) {
               mPreferencesDir = new File(getDataDir(), "shared_prefs");
           }
           return ensurePrivateDirExists(mPreferencesDir);
       }
   }

複製代碼

2.3.3 getSharedPreferences(file, mode)

# ContextImpl.java
@Override
   public SharedPreferences getSharedPreferences(File file, int mode) {
      //[見2.3.4]
       checkMode(mode);
       if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) {
           if (isCredentialProtectedStorage()
                   && !getSystemService(StorageManager.class).isUserKeyUnlocked(
                           UserHandle.myUserId())
                   && !isBuggy()) {
               throw new IllegalStateException("SharedPreferences in credential encrypted "
                       + "storage are not available until after user is unlocked");
           }
       }
       SharedPreferencesImpl sp;
       synchronized (ContextImpl.class) {
          &emsp;//獲取SharedPreferencesImpl的緩存集合[見2.3.5]
           final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
           sp = cache.get(file);
           if (sp == null) {
                //若是緩存中沒有咱們就會建立SharedPreferencesImpl實例[見2.3.6]
               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) {
           // If somebody else (some other process) changed the prefs
           // file behind our back, we reload it.  This has been the
           // historical (if undocumented) behavior.
           sp.startReloadIfChangedUnexpectedly();[見2.3.7]
       }
       return sp;
   }


複製代碼

上面的代碼有個MODE_MULTI_PROCESS模式,也就是咱們若是要在多進程時使用SharedPreferences時須要指定這個mode,可是這種方式google是不推薦使用的,由於在線上大概有萬分之一的機率形成 SharedPreferences的數據所有丟失,由於它沒有使用任何進程鎖的操做,這時從新加載可一次文件,具體見startReloadIfChangedUnexpectedly方法。bash

2.3.4 checkMode(mode)

# ContextImpl.java
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");
           }
       }
   }

複製代碼

在Android24以後的版本 SharedPreferences的mode不能再使用MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE。併發

2.3.5 getSharedPreferencesCacheLocked()

# ContextImpl.java
private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
        if (sSharedPrefsCache == null) {
            sSharedPrefsCache = new ArrayMap<>();
        }

        final String packageName = getPackageName();
        ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
        if (packagePrefs == null) {
            packagePrefs = new ArrayMap<>();
            sSharedPrefsCache.put(packageName, packagePrefs);
        }

        return packagePrefs;
    }

複製代碼

經過上面的代碼,咱們發現ContextImpl中維護了一個存儲SharedPreferencesImpl map的map緩存 sSharedPrefsCache,而且他是靜態的,也就是說整個應用獨此一份,而它的鍵是應用的包名。app

2.3.6 SharedPreferencesImpl的建立

前面講了那麼一大堆大可能是關於SharedPreferences的各類緩存流程的,以及各類前期的準備,走到這裏才真正把SharedPreferences建立出來,因爲SharedPreferences是個接口,因此它的所有實現都是由 SharedPreferencesImpl來完成的。

# SharedPreferencesImpl.java
SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        mBackupFile = makeBackupFile(file);
        mMode = mode;
        mLoaded = false;
        mMap = null;
        //[見2.3.8]
        startLoadFromDisk();
    }

複製代碼

2.3.7

當設置MODE_MULTI_PROCESS模式, 則每次getSharedPreferences過程, 會檢查SP文件上次修改時間和文件大小, 一旦全部修改則會從新加載文件.

#&emsp;SharedPreferencesImpl.java

void startReloadIfChangedUnexpectedly() {
       synchronized (mLock) {
           // TODO: wait for any pending writes to disk?
           if (!hasFileChangedUnexpectedly()) {
               return;
           }
           startLoadFromDisk();
       }
   }


   // Has the file changed out from under us?  i.e. writes that
    // we didn't instigate. private boolean hasFileChangedUnexpectedly() { synchronized (mLock) { if (mDiskWritesInFlight > 0) { // If we know we caused it, it's not unexpected.
                if (DEBUG) Log.d(TAG, "disk write in flight, not unexpected.");
                return false;
            }
        }

        final StructStat stat;
        try {
            /*
             * Metadata operations don't usually count as a block guard * violation, but we explicitly want this one. */ BlockGuard.getThreadPolicy().onReadFromDisk(); stat = Os.stat(mFile.getPath()); } catch (ErrnoException e) { return true; } synchronized (mLock) { return mStatTimestamp != stat.st_mtime || mStatSize != stat.st_size; } } 複製代碼

2.3.8 startLoadFromDisk()

這個方法的主要目的就是加載xml文件到mFile對象中,同時爲了保證這個加載過程爲異步操做,這個地方使用了線程。另外當xml文件未加載時,SharedPreferences的getString(),edit()等方法都會處於阻塞狀態(阻塞和掛起的區別...),直到mLoaded的狀態變爲true,後面的分析會驗證這一點。

private void startLoadFromDisk() {
       synchronized (mLock) {
           mLoaded = false;
       }
       new Thread("SharedPreferencesImpl-load") {
           public void run() {
                //使用線程去加載xml
               loadFromDisk();
           }
       }.start();
   }


   private void loadFromDisk() {
       synchronized (mLock) {
           if (mLoaded) {
               return;
           }
           //若是容災文件存在,則使用容災文件
           if (mBackupFile.exists()) {
               mFile.delete();
               mBackupFile.renameTo(mFile);
           }
       }

       // Debugging
       if (mFile.exists() && !mFile.canRead()) {
           Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");
       }

       Map map = null;
       StructStat stat = null;
       try {
           stat = Os.stat(mFile.getPath());
           if (mFile.canRead()) {
               BufferedInputStream str = null;
               try {
                   str = new BufferedInputStream(
                           new FileInputStream(mFile), 16*1024);
                    //從xml中全量讀取內容,保存在內存中
                   map = XmlUtils.readMapXml(str);
               } catch (Exception e) {
                   Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
               } finally {
                   IoUtils.closeQuietly(str);
               }
           }
       } catch (ErrnoException e) {
           /* ignore */
       }

       synchronized (mLock) {
           mLoaded = true;
           if (map != null) {
               mMap = map;
               mStatTimestamp = stat.st_mtime;
               mStatSize = stat.st_size;
           } else {
               mMap = new HashMap<>();
           }
           mLock.notifyAll();
       }
   }

複製代碼

這樣SharedPreference的實例建立已經完成了,而且咱們也發現SharedPreference將從文件中讀取的數據保存在了mMap的全局變量中,而後後面的讀取操做其實都只是在mMap中拿數據了,下面分析獲取數據和添加數據的流程。

3. SharedPreferences獲取數據

前面的章節已經成功的建立了SharedPreferences實例,下面看看怎麼使用它來獲取數據,下面以getString爲例分析。

3.1 getString()

@Nullable
   public String getString(String key, @Nullable String defValue) {
       synchronized (mLock) {
            //阻塞判斷,須要等到數據從xml中加載到內存中,纔會繼續執行[見3.2]
           awaitLoadedLocked();
           //直接從內存中獲取數據
           String v = (String)mMap.get(key);
           return v != null ? v : defValue;
       }
   }

複製代碼

從這裏咱們能夠驗證當咱們在上文中的結論,那就是在 SharedPreferences被建立後,咱們全部的讀取數據都是在內存中獲取的,可是這裏可能就有個疑問了,加入如今咱們put一條數據,是否要從新加載一次文件呢,其實在單進程中是不須要的,可是在多進程中就可能須要了。下面咱們繼續帶着這些疑惑去尋找答案。

3.2 awaitLoadedLocked()

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) {
           }
       }
   }

從上面的操做能夠看出當mLoaded爲false時,也就是內容沒有從xml文件中加載到內存時,該方法一直會處於阻塞狀態。   


複製代碼

4. SharedPreferences數據添加和修改

SharedPreferences中還有個Editor和EditorImpl,它們的做用是添加數據和修改數據。可是這裏要注意,咱們對Editor作操做,其實只是把數據保存在Editor的一個成員變量中,真正把數據更新到SharedPreferencesImpl而且寫入文件是在Editor的commit或者apply方法被調用以後.

4.1 EditorImpl的實現

#&emsp;SharedPreferencesImpl.java

public final class EditorImpl implements Editor {
        private final Object mLock = new Object();

        @GuardedBy("mLock")
        private final Map<String, Object> mModified = Maps.newHashMap();

        @GuardedBy("mLock")
        private boolean mClear = false;

        public Editor putString(String key, @Nullable String value) {
            synchronized (mLock) {

                mModified.put(key, value);
                return this;
            }
        }
        public Editor putStringSet(String key, @Nullable Set<String> values) {
            synchronized (mLock) {
                mModified.put(key,
                        (values == null) ? null : new HashSet<String>(values));
                return this;
            }
        }
        public Editor putInt(String key, int value) {
            synchronized (mLock) {
                mModified.put(key, value);
                return this;
            }
        }
        public Editor putLong(String key, long value) {
            synchronized (mLock) {
                mModified.put(key, value);
                return this;
            }
        }
        public Editor putFloat(String key, float value) {
            synchronized (mLock) {
                mModified.put(key, value);
                return this;
            }
        }
        public Editor putBoolean(String key, boolean value) {
            synchronized (mLock) {
                mModified.put(key, value);
                return this;
            }
        }

        public Editor remove(String key) {
            synchronized (mLock) {
                mModified.put(key, this);
                return this;
            }
        }

        public Editor clear() {
            synchronized (mLock) {
                mClear = true;
                return this;
            }
        }
    }

複製代碼

從Editor的put操做來看,它是把數據添加到mModified這個成員變量中,並未寫入文件。而寫入的操做是在commit和apply中執行的,下面就解析 SharedPreferences中兩個核心的方法commit和apply

4.2 commit和apply

commit和apply是Editor中的方法,實如今EditorImpl中,那麼他們兩有什麼區別,又是怎麼實現的呢?首先,他們兩最大的區別是commit是一個同步方法,它有一個boolean類型的返回值,而apply是一個異步方法,沒有返回值。簡單理解就是,commit須要等待提交結果,而apply不須要。因此commit以犧牲必定的性能而換來準確性的提升。另一點就是對於apply方法,官方的註釋告訴咱們不用擔憂Android組件的生命週期會對它形成的影響,底層的框架幫咱們作了處理,可是真的是這樣的嗎?[見4.2.6]分解。下面看具體的分析。

4.2.1 commit

# SharedPreferencesImpl.java

public boolean commit() {
           long startTime = 0;

           if (DEBUG) {
               startTime = System.currentTimeMillis();
           }
           //將數據保存在內存中[見4.2.3]
           MemoryCommitResult mcr = commitToMemory();
          //同步將數據寫到硬盤中[見4.2.4]
           SharedPreferencesImpl.this.enqueueDiskWrite(
               mcr, null /* sync write on this thread okay */);
           try {
              //等待寫入操做的完成
               mcr.writtenToDiskLatch.await();
           } catch (InterruptedException e) {
               return false;
           } finally {
               if (DEBUG) {
                   Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                           + " committed after " + (System.currentTimeMillis() - startTime)
                           + " ms");
               }
           }
           //用於onSharedPreferenceChanged的回調提醒
           notifyListeners(mcr);
           return mcr.writeToDiskResult;
       }


複製代碼

在commit中首先調用commitToMemory將數據保存在內存中,而後會執行寫入操做,而且讓當前commit所在的線程處於阻塞狀態。當寫入完成後會經過onSharedPreferenceChanged提醒數據發生的變化。這個過程當中有個注意的地方, mcr.writtenToDiskLatch.await(),若是非併發調用commit方法,這個操做是不須要的,可是若是併發commit時,就必須有mcr.writtenToDiskLatch.await()操做了,由於寫入操做可能會被放到別的子線程中執行.而後就是notifyListeners()方法,當咱們寫入的數據發生變化後給咱們的回調,這個回調咱們能夠經過註冊下面的代碼拿到。

sp.registerOnSharedPreferenceChangeListener { sharedPreferences, key -> }

複製代碼

4.2.2 apply

# SharedPreferencesImpl.java
public void apply() {
          final long startTime = System.currentTimeMillis();
          //將數據保存在內存中[見4.2.3]
          final MemoryCommitResult mcr = commitToMemory();
          final Runnable awaitCommit = new Runnable() {
                  public void run() {
                      try {
                          mcr.writtenToDiskLatch.await();
                      } catch (InterruptedException ignored) {
                      }

                      if (DEBUG && mcr.wasWritten) {
                          Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                                  + " applied after " + (System.currentTimeMillis() - startTime)
                                  + " ms");
                      }
                  }
              };

          QueuedWork.addFinisher(awaitCommit);

          Runnable postWriteRunnable = new Runnable() {
                  public void run() {
                      awaitCommit.run();
                      QueuedWork.removeFinisher(awaitCommit);
                  }
              };

        &emsp;// 執行文件寫入操做,傳入的 postWriteRunnable 參數不爲 null,因此在                 
          // enqueueDiskWrite 方法中會開啓子線程異步將數據寫入文件
          SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

          // Okay to notify the listeners before it's hit disk // because the listeners should always get the same // SharedPreferences instance back, which has the // changes reflected in memory. notifyListeners(mcr); } 複製代碼

apply方法的流程和commit實際上是差很少,可是apply的寫入操做會被放在一個單獨的線程中執行,而且不會阻塞當前apply所在的線程。當時有中特殊的請求是會阻塞的,那就是在Activity的onStop方法被調用,而且apply的寫入操做還未完成時,會阻塞主線程,更詳情的分析[見4.2.6]

4.2.3 commitToMemory

# SharedPreferencesImpl.java

private MemoryCommitResult commitToMemory() {
           long memoryStateGeneration;
           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); } mapToWriteToDisk = mMap; mDiskWritesInFlight++; boolean hasListeners = mListeners.size() > 0; if (hasListeners) { keysModified = new ArrayList<String>(); listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet()); } synchronized (mLock) { boolean changesMade = false; if (mClear) { if (!mMap.isEmpty()) { changesMade = true; mMap.clear(); } mClear = false; } //mModified 保存的寫記錄同步到內存中的 mMap 中 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 (!mMap.containsKey(k)) { continue; } mMap.remove(k); } else { if (mMap.containsKey(k)) { Object existingValue = mMap.get(k); if (existingValue != null && existingValue.equals(v)) { continue; } } mMap.put(k, v); } changesMade = true; if (hasListeners) { keysModified.add(k); } } // 將 mModified 同步到 mMap 以後,清空 mModified mModified.clear(); if (changesMade) { mCurrentMemoryStateGeneration++; } memoryStateGeneration = mCurrentMemoryStateGeneration; } } return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners, mapToWriteToDisk); } 複製代碼

經過上面的註釋和代碼,咱們瞭解到每次有寫操做的時候,都會同步mMap,這樣咱們就不須要每次在讀取的時候從新load文件了,可是這個結論在多進程中不適用。另外須要關注的是mDiskWritesInFlight這個變量,當mDiskWritesInFlight大於0時,會拷貝一份mMap,把它存到MemoryCommitResult類的成員mapToWriteToDisk中,而後再把mDiskWritesInFlight加1。在把mapToWriteDisk寫入到文件後,mDiskWritesInFlight會減1,因此mDiskWritesInFlight大於0說明以前已經有調用過commitToMemory了,而且尚未把map寫入到文件,這樣先後兩次要準備寫入文件的mapToWriteToDisk是兩個不一樣的內存對象,後一次調用commitToMemory時,再更新mMap中的值時不會影響前一次的mapToWriteToDisk的寫入文件

4.2.4 enqueueDiskWrite

# SharedPreferencesImpl.java

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

       // 建立Runnable,負責將數據接入文件
       final Runnable writeToDiskRunnable = new Runnable() {
               public void run() {
                   synchronized (mWritingToDiskLock) {
                      //寫入文件操做[見4.2.5]
                       writeToFile(mcr, isFromSyncCommit);
                   }
                   synchronized (mLock) {

                     // 寫入文件後將mDiskWritesInFlight值減一
                       mDiskWritesInFlight--;
                   }
                   if (postWriteRunnable != null) {
                       postWriteRunnable.run();
                   }
               }
           };

       // Typical #commit() path with fewer allocations, doing a write on
       // the current thread.
       if (isFromSyncCommit) {
           boolean wasEmpty = false;
           synchronized (mLock) {
               wasEmpty = mDiskWritesInFlight == 1;
           }
           if (wasEmpty) {

             // 當只有一個 commit 請求未處理,那麼無需開啓線程進行處理,直接在本線程執行 //writeToDiskRunnable 便可
               writeToDiskRunnable.run();
               return;
           }
       }
       //單線程執行寫入操做
       QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
   }

複製代碼

從這裏咱們能夠得出commit操做,若是隻有一次操做的時候,只會在當前線程中執行,可是若是併發commit時,剩餘的writeToDiskRunnable則會被放在單獨的線程中執行,而第一次commit所在的線程則進入阻塞狀態。它須要等後面的commit都成功後才能算真正的成功,而返回的狀態也是最後一次commit的狀態。

4.2.5 writeToFile

終於,迎來了最後真正的寫操做,包括在寫入成功的時候將容災文件刪除,或者在寫入失敗時將半成品文件刪除等,最後將寫結果保存在MemoryCommitResult中。

# SharedPreferencesImpl.java

// Note: must hold mWritingToDiskLock
   private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
       long startTime = 0;
       long existsTime = 0;
       long backupExistsTime = 0;
       long outputStreamCreateTime = 0;
       long writeTime = 0;
       long fsyncTime = 0;
       long setPermTime = 0;
       long fstatTime = 0;
       long deleteTime = 0;

       if (DEBUG) {
           startTime = System.currentTimeMillis();
       }

       boolean fileExists = mFile.exists();

       if (DEBUG) {
           existsTime = System.currentTimeMillis();

           // Might not be set, hence init them to a default value
           backupExistsTime = existsTime;
       }

       // Rename the current file so it may be used as a backup during the next read
       if (fileExists) {
           boolean needsWrite = false;

           // Only need to write if the disk state is older than this commit
           if (mDiskStateGeneration < mcr.memoryStateGeneration) {
               if (isFromSyncCommit) {
                   needsWrite = true;
               } else {
                   synchronized (mLock) {
                       // No need to persist intermediate states. Just wait for the latest state to
                       // be persisted.
                       if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
                           needsWrite = true;
                       }
                   }
               }
           }

           if (!needsWrite) {
               mcr.setDiskWriteResult(false, true);
               return;
           }

           boolean backupFileExists = mBackupFile.exists();

           if (DEBUG) {
               backupExistsTime = System.currentTimeMillis();
           }

           if (!backupFileExists) {
               if (!mFile.renameTo(mBackupFile)) {
                   Log.e(TAG, "Couldn't rename file " + mFile
                         + " to backup file " + mBackupFile);
                   mcr.setDiskWriteResult(false, false);
                   return;
               }
           } else {
               mFile.delete();
           }
       }

       // Attempt to write the file, delete the backup and return true as atomically as
       // possible.  If any exception occurs, delete the new file; next time we will restore
       // from the backup.
       try {
           FileOutputStream str = createFileOutputStream(mFile);

           if (DEBUG) {
               outputStreamCreateTime = System.currentTimeMillis();
           }

           if (str == null) {
               mcr.setDiskWriteResult(false, false);
               return;
           }
           XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);

           writeTime = System.currentTimeMillis();

           FileUtils.sync(str);

           fsyncTime = System.currentTimeMillis();

           str.close();
           ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);

           if (DEBUG) {
               setPermTime = System.currentTimeMillis();
           }

           try {
               final StructStat stat = Os.stat(mFile.getPath());
               synchronized (mLock) {
                   mStatTimestamp = stat.st_mtime;
                   mStatSize = stat.st_size;
               }
           } catch (ErrnoException e) {
               // Do nothing
           }

           if (DEBUG) {
               fstatTime = System.currentTimeMillis();
           }

           // Writing was successful, delete the backup file if there is one.
           mBackupFile.delete();

           if (DEBUG) {
               deleteTime = System.currentTimeMillis();
           }

           mDiskStateGeneration = mcr.memoryStateGeneration;

           mcr.setDiskWriteResult(true, true);

           if (DEBUG) {
               Log.d(TAG, "write: " + (existsTime - startTime) + "/"
                       + (backupExistsTime - startTime) + "/"
                       + (outputStreamCreateTime - startTime) + "/"
                       + (writeTime - startTime) + "/"
                       + (fsyncTime - startTime) + "/"
                       + (setPermTime - startTime) + "/"
                       + (fstatTime - startTime) + "/"
                       + (deleteTime - startTime));
           }

           long fsyncDuration = fsyncTime - writeTime;
           mSyncTimes.add(Long.valueOf(fsyncDuration).intValue());
           mNumSync++;

           if (DEBUG || mNumSync % 1024 == 0 || fsyncDuration > MAX_FSYNC_DURATION_MILLIS) {
               mSyncTimes.log(TAG, "Time required to fsync " + mFile + ": ");
           }

           return;
       } catch (XmlPullParserException e) {
           Log.w(TAG, "writeToFile: Got exception:", e);
       } catch (IOException e) {
           Log.w(TAG, "writeToFile: Got exception:", e);
       }

       // Clean up an unsuccessfully written file
       if (mFile.exists()) {
           if (!mFile.delete()) {
               Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
           }
       }
       mcr.setDiskWriteResult(false, false);
   }

複製代碼

4.2.6 apply引發的anr

還記得在介紹apply時,咱們瞭解到apply是異步的,不會阻塞咱們的主線程,官方的註釋頁說過android組件的生命週期不會對aplly的異步寫入形成影響,告訴咱們不用擔憂,但它卻會有必定的概率引發anr,好比有一種狀況,當咱們的Activity執行onPause()的時候,也就是ActivityThread類執行handleStopActivity方法是,看看它幹了啥 它會執行 QueuedWork.waitToFinish()方法,而waitToFinish方法中有個while循環,若是咱們還有沒有完成的異步落盤操做時,它會調用到咱們在apply方法中建立的awaitCommit,讓咱們主線程處於等待狀態,直到全部的落盤操做完成,纔會跳出循環,這也就是apply形成anr的元兇。

# ActivityThread.java

@Override
  public void handleStopActivity(IBinder token, boolean show, int configChanges,
          PendingTransactionActions pendingActions, boolean finalStateRequest, String reason) {
      //...省略

      // Make sure any pending writes are now committed.
      if (!r.isPreHoneycomb()) {
          QueuedWork.waitToFinish();
      }
     //...省略
  }

複製代碼
/**
   * Trigger queued work to be processed immediately. The queued work is processed on a separate
   * thread asynchronous. While doing that run and process all finishers on this thread. The
   * finishers can be implemented in a way to check weather the queued work is finished.
   *
   * Is called from the Activity base class's onPause(), after BroadcastReceiver's onReceive,
   * after Service command handling, etc. (so async work is never lost)
   */
  public static void waitToFinish() {
      ...省略
      try {
          while (true) {
              Runnable finisher;

              synchronized (sLock) {
                  finisher = sFinishers.poll();
              }

              if (finisher == null) {
                  break;
              }
              [見4.2.2中的awaitCommit]  
              finisher.run();
          }
      } finally {
          sCanDelay = true;
      }
      ...省略
  }


複製代碼

總結

SharedPreferences是一種輕量級的存儲方式,使用方便,可是也有它適用的場景。要優雅滴使用sp,要注意如下幾點:

  1. 不一樣的配置信息不要都放在一塊兒,這樣每次讀寫會愈來愈卡。
  2. 不要在同一個文件中頻繁的讀取key和value,由於同步鎖的緣故,會形成卡頓
  3. 不要頻繁的commit和apply,儘可能批量修改一次提交,尤爲是apply,會形成anr
  4. 不要在保存太大的數據
  5. 不要期望它在多進程中使用。

參考文獻

  1. SharedPreference的讀寫原理分析

  2. 一眼看穿 SharedPreferences

  3. 完全搞懂 SharedPreferences

  4. StrictMode解析

相關文章
相關標籤/搜索