每日一問:談談 SharedPreferences 的 apply() 和 commit()

SharedPreferences 應該是任何一名 Android 初學者都知道的存儲類了,它輕量,適合用於保存軟件配置等參數。以鍵值對的 XML 文件形式存儲在本地,程序卸載後也會一併清除,不會殘留信息。java

使用起來也很是簡單。android

// 讀取
val sharedPreferences = getSharedPreferences("123", Context.MODE_PRIVATE)
val string = sharedPreferences.getString("123","")
// 寫入
val editor = sharedPreferences.edit()
editor.putString("123","123")
editor.commit()

當咱們寫下這樣的代碼的時候,IDE 極易出現一個警告,提示咱們用 apply() 來替換 commit()。緣由也很簡單,由於 commit() 是同步的,而 apply() 採用異步的方式一般來講效率會更高一些。可是,當咱們把 editor.commit() 的返回值賦給一個變量的時候,這時候就會發現 IDE 沒有了警告。這是由於 IDE 認爲咱們想要使用 editor.commit() 的返回值了,因此,一般來講,在咱們不關心操做結果的時候,咱們更傾向於使用 apply() 進行寫入的操做。c++

獲取 SharedPreferences 實例

咱們能夠經過 3 種方式來獲取 SharedPreferences 的實例。
首先固然是咱們最多見的寫法。c#

getSharedPreferences("123", Context.MODE_PRIVATE)

Context 的任意子類均可以直接經過 getSharedPreferences() 方法獲取到 SharedPreferences 的實例,接受兩個參數,分別對應 XML 文件的名字和操做模式。其中 MODE_WORLD_READABLEMODE_WORLD_WRITEABLE 這兩種模式已在 Android 4.2 版本中被廢棄。api

  • Context.MODE_PRIVATE: 指定該 SharedPreferences 數據只能被本應用程序讀、寫;
  • Context.MODE_WORLD_READABLE: 指定該 SharedPreferences 數據能被其餘應用程序讀,但不能寫;
  • Context.MODE_WORLD_WRITEABLE: 指定該 SharedPreferences 數據能被其餘應用程序讀;
  • Context.MODE_APPEND:該模式會檢查文件是否存在,存在就往文件追加內容,不然就建立新文件;

另外在 Activity 的實現中,還能夠直接經過 getPreferences() 獲取,實際上也就把當前 Activity 的類名做爲文件名參數。緩存

public SharedPreferences getPreferences(@Context.PreferencesMode int mode) {
    return getSharedPreferences(getLocalClassName(), mode);
}

此外,咱們也能夠經過 PreferenceManagergetDefaultSharedPreferences() 獲取到。安全

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

public static String getDefaultSharedPreferencesName(Context context) {
    return context.getPackageName() + "_preferences";
}

private static int getDefaultSharedPreferencesMode() {
    return Context.MODE_PRIVATE;
}

能夠很明顯的看到,這個方式就是在直接把當前應用的包名做爲前綴來進行命名的。多線程

注意:若是在 Fragment 中使用 SharedPreferences 時,SharedPreferences 的初始化儘可能放在 onAttach(Activity activity) 裏面進行 ,不然可能會報空指針,即 getActivity() 會可能返回爲空。app

SharedPreferences 源碼(基於 API 28)

有較多 SharedPreferences 使用經驗的人,就會發現 SharedPreferences 其實具有挺多的坑,但這些坑主要都是由於不熟悉其中真正的原理所致使的,因此,筆者在這裏,帶你們一塊兒揭開 SharedPreferences 的神祕面紗。異步

SharedPreferences 實例獲取

前面講了 SharedPreferences 有三種獲取實例的方法,但歸根結底都是調用的 ContextgetSharedPreferences() 方法。因爲 Android 的 Context 類採用的是裝飾者模式,而裝飾者對象其實就是 ContextImpl,因此咱們來看看源碼是怎麼實現的。

// 存放的是名稱和文件夾的映射,實際上這個名稱就是咱們外面傳進來的 name
private ArrayMap<String, File> mSharedPrefsPaths;

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<>();
        }
        file = mSharedPrefsPaths.get(name);
        if (file == null) {
            file = getSharedPreferencesPath(name);
            mSharedPrefsPaths.put(name, file);
        }
    }
    return getSharedPreferences(file, mode);
}

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

private File makeFilename(File base, String name) {
    if (name.indexOf(File.separatorChar) < 0) {
        return new File(base, name);
    }
    throw new IllegalArgumentException(
            "File " + name + " contains a path separator");
}

能夠很明顯的看到,內部是採用 ArrayMap 來作的處理,而這個 mSharedPrefsPaths 主要是用於存放名稱和文件夾的映射,實際上這個名稱就是咱們外面傳進來的 name,這時候咱們經過 name 拿到咱們的 File,若是當前池子中沒有的話,則直接新建一個 File,並放入到 mSharedPrefsPaths 中。最後仍是調用的重載方法 getSharedPreferences(File,mode)

// 存放包名與ArrayMap鍵值對,初始化時會默認以包名做爲鍵值對中的 Key,注意這是個 static 變量
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;

@Override
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) {
            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");
                }
            }
            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();
    }
    return sp;
}   

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;
}

能夠看到,又採用了一個 ArrayMap 來存放文件和 SharedPreferencesImpl 組成的鍵值對,而後經過經過單例的方式返回一個 SharedPreferences 對象,其實是 SharedPreferences 的實現類 SharedPreferencesImpl,並且在其中還創建了一個內部緩存機制。

因此,從上面的分析中,咱們能知道 對於一個相同的 name,咱們獲取到的都是同一個 SharedPreferencesImpl 對象。

SharedPreferencesImpl

在上面的操做中,咱們能夠看到在第一次調用 getSharedPreferences 的時候,咱們會去構造一個 SharedPreferencesImpl 對象,咱們來看看都作了什麼。

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();
}

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<String, Object> map = null;
    StructStat stat = null;
    Throwable thrown = null;
    try {
        stat = Os.stat(mFile.getPath());
        if (mFile.canRead()) {
            BufferedInputStream str = null;
            try {
                str = new BufferedInputStream(
                        new FileInputStream(mFile), 16 * 1024);
                map = (Map<String, Object>) XmlUtils.readMapXml(str);
            } catch (Exception e) {
                Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
            } finally {
                IoUtils.closeQuietly(str);
            }
        }
    } catch (ErrnoException e) {
        // An errno exception means the stat failed. Treat as empty/non-existing by
        // ignoring.
    } catch (Throwable t) {
        thrown = t;
    }

    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();
        }
    }
}

注意看咱們的 startLoadFromDisk 方法,咱們會去新開一個子線程,而後去經過 XmlUtils.readMapXml() 方法把指定的 SharedPreferences 文件的全部的鍵值對都讀出來,而後存放到一個 map 中。

而衆所周知,文件的讀寫操做都是耗時的,可想而知,在咱們第一次去讀取一個 SharedPreferences 文件的時候花上了太多的時間會怎樣。

SharedPreferences 的讀取操做

上面講了初次獲取一個文件的 SharedPreferences 實例的時候,會先去把全部鍵值對讀取到緩存中,這明顯是一個耗時操做,而咱們正常的去讀取數據的時候,都是相似這樣的代碼。

val sharedPreferences = getSharedPreferences("123", Context.MODE_PRIVATE)
val string = sharedPreferences.getString("123","")

SharedPreferencesgetXXX() 方法可能會報 ClassCastException 異常,因此咱們在同一個 name 的時候,對不同的類型,必須使用不一樣的 key。可是 putXXX 是能夠用不一樣的類型值覆蓋相同的 key 的。

那勢必可能會致使這個操做須要等待必定的時間,咱們姑且能夠這麼猜測,在 getXXX() 方法執行的時候應該是會等待前面的操做完成才能執行的。

由於 SharedPreferences 是一個接口,因此咱們主要來看看它的實現類 SharedPreferencesImpl,這裏以 getString() 爲例。

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

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) {
        }
    }
    if (mThrowable != null) {
        throw new IllegalStateException(mThrowable);
    }
}

能夠看到,在 awaitLoadedLocked 方法裏面咱們使用了 mLock.wait() 來等待初始化的讀取操做,而咱們前面看到的 loadFromDiskLocked() 方法的最後也能夠看到它調用了 mLock.notifyAll() 方法來喚醒後面這個阻塞的 getXXX()那麼這裏就會明顯出現一個問題,咱們的 getXXX() 方法是寫在 UI 線程的,若是這個方法被阻塞的過久,勢必會出現 ANR 的狀況。因此咱們必定在平時須要根據具體狀況考慮是否須要把 SharedPreferences 的讀寫操做放在子線程中。

SharedPreferences 的內部類 Editor

咱們在寫入數據以前,老是要先經過相似這樣的代碼獲取 SharedPreferences 的內部類 Editor

val editor = sharedPreferences.edit()

咱們固然要看看這個究竟是什麼東西。

@Override
    public Editor edit() {
        // TODO: remove the need to call awaitLoadedLocked() when
        // requesting an editor.  will require some work on the
        // Editor, but then we should be able to do:
        //
        //      context.getSharedPreferences(..).edit().putString(..).apply()
        //
        // ... all without blocking.
        synchronized (mLock) {
            awaitLoadedLocked();
        }

        return new EditorImpl();
    }

咱們在

能夠看到,咱們在讀取解析完 XML 文件的時候,直接返回了一個 Editor 的實現類 EditorImpl。咱們隨便查看一個 putXXX 的方法一看。

private final Object mEditorLock = new Object();

@GuardedBy("mEditorLock")
private final Map<String, Object> mModified = new HashMap<>();

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

@Override
public Editor putString(String key, @Nullable String value) {
    synchronized (mEditorLock) {
        mModified.put(key, value);
        return this;
    }
}

能夠看到,咱們在 EditorImpl 裏面使用了一個 HashMap 來存放咱們的鍵值對數據,每次 put 的時候都會直接往這個鍵值對變量 mModified 中進行數據的 put 操做。

commit() 和 apply()

咱們老是在更新數據後須要加上 commit() 或者 apply() 來進行輸入的寫入操做,咱們不妨來看看他們的實現到底有什麼區別。

先看 commit() 和 apply() 的源碼。

@Override
public boolean commit() {
    long startTime = 0;

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

    MemoryCommitResult mcr = commitToMemory();

    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");
        }
    }
    notifyListeners(mcr);
    return mcr.writeToDiskResult;
}

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

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

    QueuedWork.addFinisher(awaitCommit);

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

    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() 的區別是在 commit() 把內容同步提交到了硬盤,而 apply() 是先當即把修改提交給了內存,而後開啓了一個異步的線程提交到硬盤。commit() 會接收 MemoryCommitResult 裏面的一個 boolean 參數做爲結果,而 apply() 沒有對結果作任何關心。

咱們能夠看到,文件寫入更新的操做都是交給 commitToMemory() 作的,這個方法返回了一個 MemoryCommitResult 對象,咱們來看看到底作了什麼。

// Returns true if any changes were made
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 (mEditorLock) {
            boolean changesMade = false;

            if (mClear) {
                if (!mapToWriteToDisk.isEmpty()) {
                    changesMade = true;
                    mapToWriteToDisk.clear();
                }
                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);
                }
            }

            mModified.clear();

            if (changesMade) {
                mCurrentMemoryStateGeneration++;
            }

            memoryStateGeneration = mCurrentMemoryStateGeneration;
        }
    }
    return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
            mapToWriteToDisk);
}

能夠看到,咱們這裏的 mMap 即存放當前 SharedPreferences 文件中的鍵值對,而 mModified 則存放的是當時 edit() 時 put 進去的鍵值對,這個咱們前面有所介紹。這裏有個 mDiskWritesInFlight 看起來應該是表示正在等待寫的操做數量。

接下來咱們首先處理了 edit().clear() 操做的 mClear 標誌,當咱們在外面調用 clear() 方法的時候,咱們會把 mClear 設置爲 true,這時候咱們會直接經過 mMap.clear() 清空此時文件中的鍵值對,而後再遍歷 mModified 中新 put 進來的鍵值對數據放到 mMap 中。也就是說:在一次提交中,若是咱們又有 put 又有 clear() 操做的話,咱們只能 clear() 掉以前的鍵值對,此次 put() 進去的鍵值對仍是會被寫入到 XML 文件中。

// 讀取
val sharedPreferences = getSharedPreferences("123", Context.MODE_PRIVATE)
// 寫入
val editor = sharedPreferences.edit()
editor.putInt("1", 123)
editor.clear()
editor.apply()
Log.e("nanchen2251", "${sharedPreferences.getInt("1", 0)}")

也就是說,當咱們編寫下面的代碼的時候,獲得的打印仍是 123。

而後咱們接着往下看,又發現了另一個 commit()apply() 都作了調用的方法是 enqueueDiskWrite()

/**
 * Enqueue an already-committed-to-memory result to be written
 * to disk.
 *
 * They will be written to disk one-at-a-time in the order
 * that they're enqueued.
 *
 * @param postWriteRunnable if non-null, we're being called
 *   from apply() and this is the runnable to run after
 *   the write proceeds.  if null (from a regular commit()),
 *   then we're allowed to do this disk write on the main
 *   thread (which in addition to reducing allocations and
 *   creating a background thread, this has the advantage that
 *   we catch them in userdebug StrictMode reports to convert
 *   them where possible to apply() ...)
 */
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);
                }
                synchronized (mLock) {
                    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) {
            writeToDiskRunnable.run();
            return;
        }
    }

    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

在這個方法中,首先經過判斷 postWriteRunnable 是否爲 null 來判斷是 apply() 仍是 commit()。而後定義了一個 Runnable 任務,在 Runnable 中先調用了 writeToFile() 進行了寫入和計數器更新的操做。

而後咱們再來看看這個 writeToFile() 方法作了些什麼。

@GuardedBy("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_mtim;
                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((int) fsyncDuration);
        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);
}

代碼比較長,作了一些時間的記錄和 XML 的相關處理,但最值得咱們關注的仍是其中打了標註的對於 mBackupFile 的處理。咱們能夠明顯地看到,在咱們寫入文件的時候,咱們會把此前的 XML 文件更名爲一個備份文件,而後再將要寫入的數據寫入到一個新的文件中。若是這個過程執行成功的話,就會把備份文件刪除。因而可知:即便咱們每次只是添加一個鍵值對,也會從新寫入整個文件的數據,這也說明了 SharedPreferences 只適合保存少許數據,文件太大會有性能問題。

看完了這個 writeToFile() ,咱們再來看看下面作了啥。

// 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) {
        writeToDiskRunnable.run();
        return;
    }
}

QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);

能夠看到,當且僅當是 commit() 而且只有一個待寫入操做的時候才能直接執行到 writeToDiskRunnable.run(),不然都會執行到 QueuedWorkqueue() 方法,這個 QueuedWork 又是什麼東西?

/** Finishers {@link #addFinisher added} and not yet {@link #removeFinisher removed} */
@GuardedBy("sLock")
private static final LinkedList<Runnable> sFinishers = new LinkedList<>();

/** Work queued via {@link #queue} */
@GuardedBy("sLock")
private static final LinkedList<Runnable> sWork = new LinkedList<>();
/**
 * Internal utility class to keep track of process-global work that's outstanding and hasn't been
 * finished yet.
 *
 * New work will be {@link #queue queued}.
 *
 * It is possible to add 'finisher'-runnables that are {@link #waitToFinish guaranteed to be run}.
 * This is used to make sure the work has been finished.
 *
 * This was created for writing SharedPreference edits out asynchronously so we'd have a mechanism
 * to wait for the writes in Activity.onPause and similar places, but we may use this mechanism for
 * other things in the future.
 *
 * The queued asynchronous work is performed on a separate, dedicated thread.
 *
 * @hide
 */
public class QueuedWork {
     /**
     * Add a finisher-runnable to wait for {@link #queue asynchronously processed work}.
     *
     * Used by SharedPreferences$Editor#startCommit().
     *
     * Note that this doesn't actually start it running.  This is just a scratch set for callers
     * doing async work to keep updated with what's in-flight. In the common case, caller code
     * (e.g. SharedPreferences) will pretty quickly call remove() after an add(). The only time
     * these Runnables are run is from {@link #waitToFinish}.
     *
     * @param finisher The runnable to add as finisher
     */
    public static void addFinisher(Runnable finisher) {
        synchronized (sLock) {
            sFinishers.add(finisher);
        }
    }

    /**
     * Remove a previously {@link #addFinisher added} finisher-runnable.
     *
     * @param finisher The runnable to remove.
     */
    public static void removeFinisher(Runnable finisher) {
        synchronized (sLock) {
            sFinishers.remove(finisher);
        }
    }

    /**
     * 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() {
        long startTime = System.currentTimeMillis();
        boolean hadMessages = false;

        Handler handler = getHandler();

        synchronized (sLock) {
            if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
                // Delayed work will be processed at processPendingWork() below
                handler.removeMessages(QueuedWorkHandler.MSG_RUN);

                if (DEBUG) {
                    hadMessages = true;
                    Log.d(LOG_TAG, "waiting");
                }
            }

            // We should not delay any work as this might delay the finishers
            sCanDelay = false;
        }

        StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
        try {
            processPendingWork();
        } finally {
            StrictMode.setThreadPolicy(oldPolicy);
        }

        try {
            while (true) {
                Runnable finisher;

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

                if (finisher == null) {
                    break;
                }

                finisher.run();
            }
        } finally {
            sCanDelay = true;
        }

        synchronized (sLock) {
            long waitTime = System.currentTimeMillis() - startTime;

            if (waitTime > 0 || hadMessages) {
                mWaitTimes.add(Long.valueOf(waitTime).intValue());
                mNumWaits++;

                if (DEBUG || mNumWaits % 1024 == 0 || waitTime > MAX_WAIT_TIME_MILLIS) {
                    mWaitTimes.log(LOG_TAG, "waited: ");
                }
            }
        }
    }
    /**
     * Queue a work-runnable for processing asynchronously.
     *
     * @param work The new runnable to process
     * @param shouldDelay If the message should be delayed
     */
    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);
            }
        }
    }
}

簡單地說,這個 QueuedWork 類裏面有一個專門存放 Runnable 的兩個 LinkedList 對象,他們分別對應未完成的操做 sFinishers 和正在工做的 sWork
咱們在 waitToFinish() 方法中,會不斷地去遍歷執行未完成的 Runnable。咱們根據註釋也知道了這個方法會在 ActivityonPause()BroadcastReceiveronReceive() 方法後調用。假設咱們頻繁的調用了 apply()方法,並緊接着調用了 onPause() ,那麼就可能會發生 onPause() 一直等待 QueuedWork.waitToFinish 執行完成而產生 ANR。也就是說,即便是調用了 apply() 方法去異步提交,也不是徹底安全的。若是 apply() 方法使用不當,也是可能出現 ANR 的。

總結

說了這麼多,咱們固然仍是須要作一個總結。

  1. apply() 沒有返回值而 commit() 返回 boolean 代表修改是否提交成功 ;
  2. commit() 是把內容同步提交到硬盤的,而 apply() 先當即把修改提交到內存,而後開啓一個異步的線程提交到硬盤,而且若是提交失敗,你不會收到任何通知。
  3. 全部 commit() 提交是同步過程,效率會比 apply() 異步提交的速度慢,在不關心提交結果是否成功的狀況下,優先考慮 apply() 方法。
  4. apply() 是使用異步線程寫入磁盤,commit() 是同步寫入磁盤。因此咱們在主線程使用的 commit() 的時候,須要考慮是否會出現 ANR 問題。
  5. 咱們每次添加鍵值對的時候,都會從新寫入整個文件的數據,因此它不適合大量數據存儲。
  6. 多線程場景下效率比較低,由於 get 操做的時候,會鎖定 SharedPreferencesImpl 裏面的對象,互斥其餘操做,而當 putcommit()apply() 操做的時候都會鎖住 Editor 的對象,在這樣的狀況下,效率會下降。
  7. 因爲每次都會把整個文件加載到內存中,所以,若是 SharedPreferences 文件過大,或者在其中的鍵值對是大對象的 JSON 數據則會佔用大量內存,讀取較慢是一方面,同時也會引起程序頻繁 GC,致使的界面卡頓。

基於以上缺點:

  1. 建議不要存儲較大數據到 SharedPreferences,也不要把較多數據存儲到同一個 name 對應的 SharedPreferences 中,最好根據規則拆分爲多個 SharedPreferences 文件。
  2. 頻繁修改的數據修改後統一提交,而不是修改事後立刻提交。
  3. 在跨進程通信中不去使用 SharedPreferences
  4. 獲取 SharedPreferences 對象的時候會讀取 SharedPreferences 文件,若是文件沒有讀取完,就執行了 get 和 put 操做,可能會出現須要等待的狀況,所以最好提早獲取 SharedPreferences 對象。
  5. 每次調用 edit() 方法都會建立一個新的 EditorImpl 對象,不要頻繁調用 edit() 方法。
    參考連接:https://juejin.im/post/5adc444df265da0b886d00bc#heading-10
相關文章
相關標籤/搜索