1. 程式人生 > >每日一問:談談 SharedPreferences 的 apply() 和 commit()

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

SharedPreferences 應該是任何一名 Android 初學者都知道的儲存類了,它輕量,適合用於儲存軟體配置等引數。以鍵值對的 XML 檔案形式儲存在本地,程式解除安裝後也會一併清除,不會殘留資訊。

使用起來也非常簡單。

// 讀取
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() 進行寫入的操作。

獲取 SharedPreferences 例項

我們可以通過 3 種方式來獲取 SharedPreferences

的例項。
首先當然是我們最常見的寫法。

getSharedPreferences("123", Context.MODE_PRIVATE)

Context 的任意子類都可以直接通過 getSharedPreferences() 方法獲取到 SharedPreferences 的例項,接受兩個引數,分別對應 XML 檔案的名字和操作模式。其中 MODE_WORLD_READABLEMODE_WORLD_WRITEABLE 這兩種模式已在 Android 4.2 版本中被廢棄。

  • 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() 會可能返回為空。

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