SharePreferences原始碼分析(commit與apply的區別以及原理)

許佳佳233發表於2016-12-25

#前提概要
上一篇文章SharePreferences原始碼分析(SharedPreferencesImpl),筆者分析了SharedPreferencesImpl的原理,然而結尾有讀者評論說想通過原始碼理解一下commit()與apply()的區別。由於上篇文章已經發布,就不特地加長篇幅了,在此新啟一篇分析一下兩者的區別。
如果對SharedPreferencesImpl的原理還是完全不瞭解的建議看一下上一篇文章,此篇文章主要只講commit()與apply()的區別。
#官方解釋
近期Google Developers 中國網站已經正式釋出,我們就去官網上看一下關於apply的解釋。

apply

Added in API level 9 void apply () Commit your preferences changesback from this Editor to the SharedPreferences object it is editing.This atomically performs the requested modifications, replacing whatever is currently in the SharedPreferences.

Note that when two editors are modifying preferences at the same time,the last one to call apply wins.

Unlike commit(), which writes its preferences out to persistent storage synchronously, apply() commits its changes to the in-memory SharedPreferences immediately but starts an asynchronous commit to disk and you won’t be notified of any failures. If another editor on this SharedPreferences does a regular commit() while a apply() is still outstanding, the commit() will block until all async commits are completed as well as the commit itself.

As SharedPreferences instances are singletons within a process, it’s safe to replace any instance of commit() with apply() if you were already ignoring the return value.

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.

The SharedPreferences.Editor interface isn’t expected to be implemented directly. However, if you previously did implement it and are now getting errors about missing apply(), you can simply call commit() from apply().

通過上面,我們翻譯一下,可以大概得到以下幾點:
1、如果先後apply()了幾次,那麼會以最後一次apply()的為準。
2、commit()是把內容同步提交到硬碟的。而apply()先立即把修改提交到記憶體,然後開啟一個非同步的執行緒提交到硬碟,並且如果提交失敗,你不會收到任何通知。
3、如果當一個apply()的非同步提交還在進行的時候,執行commit()操作,那麼commit()是會阻塞的。而如果commit()的時候,前面的commit()還未結束,這個commit()還是會阻塞的。(所以引起commit阻塞會有這兩種原因)
4、由於SharePreferences在一個程式中的例項一般都是單例的,所以如果你不是很在意返回值的話,你使用apply()代替commit()是無所謂的。

#原始碼分析

        public void apply() {
            final MemoryCommitResult mcr = commitToMemory();
            final Runnable awaitCommit = new Runnable() {
                    public void run() {
                        try {
                            mcr.writtenToDiskLatch.await();
                        } catch (InterruptedException ignored) {
                        }
                    }
                };

            QueuedWork.add(awaitCommit);

            Runnable postWriteRunnable = new Runnable() {
                    public void run() {
                        awaitCommit.run();
                        QueuedWork.remove(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);
        }
        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;
            }
            notifyListeners(mcr);
            return mcr.writeToDiskResult;
        }

程式碼其實差不多,主要有兩點不同:
1、commit()有返回值,apply()沒有返回值。正是驗證了官方的解釋:apply()失敗了是不會報錯的。
2、有一行程式碼在commit()中是直接執行的,而在apply()中是放到了Runnable中,這行程式碼意思是等待檔案寫完:

mcr.writtenToDiskLatch.await();

為什麼放到Runnable中,其實比較好推測,就是想放到執行緒池中執行唄,當然這僅僅是我們的推測,我們需要找到具體的程式碼證明。那麼我們就檢視呼叫這個Runnable的enqueueDiskWrite()方法:

    private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
        final Runnable writeToDiskRunnable = new Runnable() {
                public void run() {
                    synchronized (mWritingToDiskLock) {
                        writeToFile(mcr);
                    }
                    synchronized (SharedPreferencesImpl.this) {
                        mDiskWritesInFlight--;
                    }
                    if (postWriteRunnable != null) {
                        postWriteRunnable.run();
                    }
                }
            };

        final boolean isFromSyncCommit = (postWriteRunnable == null);

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

        QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
    }

如上,我們可以看到writeToFile()也就是寫入檔案的邏輯我們會放到writeToDiskRunnable 這個Runnable中,如果沒有傳遞postWriteRunnable進來(也就是commit的情況),那麼就會在當前執行緒執行寫入檔案操作,而如果傳遞了postWriteRunnable進來(也就是apply的情況),那麼就會把寫入檔案的邏輯放到執行緒池中執行。
這裡也是驗證了官方的說明:apply()寫入檔案的操作是非同步的,而commit()的寫入檔案的操作是在當前執行緒同步執行的。

關於寫入檔案操作的具體分析此處就不多說增加篇幅了,有興趣的讀者可以看上一篇文章
SharePreferences原始碼分析(SharedPreferencesImpl)
#總結
從原始碼來分析其實很簡單,兩者主要區別有兩點:
1、commit()有返回值,apply()沒有返回值。apply()失敗了是不會報錯的。
2、apply()寫入檔案的操作是非同步的,會把Runnable放到執行緒池中執行,而commit()的寫入檔案的操作是在當前執行緒同步執行的。
因此當兩者都可以使用的時候還是推薦使用apply(),因為apply()寫入檔案操作是非同步執行的,不會佔用主執行緒資源。

相關文章