在多程序中使用 SharedPreference
之前都是使用 SharedPreference 來做一些基本的儲存工作,因為都是在同一程序下使用,所以也沒有遇到過什麼問題,這次偶然間需要在多程序下使用,結果發現在讀取時會存在讀取不到的問題,因此去看看了原始碼,找到了問題原因和解決方式,也對 SharedPreference 有了更深的理解,特此記錄一下~
獲取 SharedPreference
通常我們都是通過Context.getSharedPreferences()
來獲取 SharedPreference 物件,這個 Context 無論是 Application、 Service 或是 Activity,都是繼承自 ContextWrapper,通過檢視 ContextWrapper 原始碼可以看發現內部都是呼叫了 mBase 的相關方法,而這個 mBase 就是 ContextImpl。getSharedPreferences()
在 ContextImpl 有兩個過載方法
public SharedPreferences getSharedPreferences(String name, int mode) { ... 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); }
這個是我們常用的,其中 name 為檔名稱,也就是生成後儲存在 data 目錄下的 xml 檔名稱,mode 為操作模式,通常我們傳入的都是Context.MODE_PRIVATE
,這裡只是去獲取 file 檔案,然後呼叫getSharedPreferences(file, mode)
方法
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) { ... 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) { sp.startReloadIfChangedUnexpectedly(); } return sp; }
可以看到,在獲取 SharedPreference 時,系統會其做一個快取,因此不會每次都去 新建一個出來,減少了不必要的開銷,這裡要特別注意最後一段,當 mode 為Context.MODE_MULTI_PROCESS
或是 Android 版本低於 Android 3.0時,會去執行startReloadIfChangedUnexpectedly()
方法,這個地方就是在多程序下可以使用的原因,後續再說。
get
呼叫 SharedPreference 的各種 get 方法,其實是從記憶體中去拿資料,這裡以getString()
為例
public String getString(String key, @Nullable String defValue) { synchronized (mLock) { awaitLoadedLocked(); String v = (String)mMap.get(key); return v != null ? v : defValue; } }
其中awaitLoadedLocked()
的作用保證資料已經從檔案中載入到記憶體中,mLoaded
在loadFromDisk()
中載入完成後,即 Map 被賦值後被置為 true
private void awaitLoadedLocked() { .... while (!mLoaded) { try { mLock.wait(); } catch (InterruptedException unused) { } } }
commit 和 apply
在獲取到 SharedPreference 後,要想儲存資料,必須要呼叫edit()
方法來獲取一個 Editor 物件,Editor 是 SharedPreference 內的一個介面,提供了所有的 put、提交以及清除的方法,它的實現是 EditorImpl。
commit()
方法帶有一個布林的返回值,用來返回是否成功將提交寫入檔案中,而且是同步寫入,因此如果要寫入的資料過大,會造成執行緒阻塞,apply()
方法沒有返回值,用非同步方式寫入,也是比較推薦的一種用法。
public boolean commit() { MemoryCommitResult mcr = commitToMemory(); SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null /* sync write on this thread okay */); try { //使用 CountDownLatch 來做等待操作 mcr.writtenToDiskLatch.await(); } catch (InterruptedException e) { return false; } notifyListeners(mcr); return mcr.writeToDiskResult; }
public void apply() { final MemoryCommitResult mcr = commitToMemory(); .... SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable); notifyListeners(mcr); }
可以看到兩個方法中都是先通過呼叫commitToMemory()
獲取到了一個 MemoryCommitResult 物件,commitToMemory()
主要是將 Editor 中的更改新增到 SharedPreference 的快取 Map 中去,如果呼叫了Clear()
,則會去對 Map 做清空操作,接著會遍歷更改寫入到 Map 中,最後返回一個 MemoryCommitResult 物件
return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,mapToWriteToDisk);
其中 memoryStateGeneration 是一個長整型,用來記錄當前記憶體的狀態,會在每次修改後加一,keysModified 是所有更改的 key 值,listeners 是通過registerOnSharedPreferenceChangeListener()
註冊的 Listener 集合,mapToWriteToDisk是修改後需要寫入磁碟的 Map。在獲取到 MemoryCommitResult 後,會將其傳入 SharedPreference 的enqueueDiskWrite()
方法中
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(); } } }; if (isFromSyncCommit) { boolean wasEmpty = false; synchronized (mLock) { wasEmpty = mDiskWritesInFlight == 1; } if (wasEmpty) { writeToDiskRunnable.run(); return; } } QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit); }
這裡要注意的是 mDiskWritesInFlight 是在commitToMemory()
做加一操作,因此,如果傳入的 postWriteRunnable 為空,則 wasEmpty 肯定為true,因此commit()
方法會同步寫入,否則會將 postWriteRunnable 傳入到QueuedWork.queue()
中去
LinkedList<Runnable> sWork = new LinkedList<>(); 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); } } }
在queue ()
中會通過 getHandler 來獲取到一個 Handler,然後通過這個Handler傳送一條訊息,其實這裡就是apply()
是非同步寫入的關鍵,通過檢視getHandler()
程式碼,發現裡面就是通過 HandlerThread 來獲取這個 Handler,因此也完成了執行緒的切換
private static Handler getHandler() { synchronized (sLock) { if (sHandler == null) { HandlerThread handlerThread = new HandlerThread("queued-work-looper", Process.THREAD_PRIORITY_FOREGROUND); handlerThread.start(); sHandler = new QueuedWorkHandler(handlerThread.getLooper()); } return sHandler; } }
Mode
-
Context.MODE_PRIVATE
表明只能被當前應用讀寫或是分享同一 user ID 的所有應用讀寫 -
Context.MODE_MULTI_PROCESS
已被標記為棄用,隨時可能會被移除,如果在多程序下官方推薦使用ContentProvider
來進行資料共享
多程序中使用
首先來分析下為什麼會出現獲取資料為空的情況,之前在看Context.getSharedPreferences()
時,可以看到 Context 會對 SharedPreference 做一個快取,即只會在第一次獲取時才會新建立物件,因此,對應的 SharedPreference 建構函式中的startLoadFromDisk()
也只有在第一次才會呼叫,那麼,問題就來了,當你在主程序中新增或修改了資料,而在程序2中已經獲取過對應的SharedPreference,這時在程序2中去呼叫 get 方法,因為程序2記憶體中儲存的 Map 中資料並未更改,所以返回空資料或舊資料。
而當我們把 mode 改為Context.MODE_MULTI_PROCESS
時為什麼就可以獲取到正確的資料了呢?主要原因就在getSharedPreferences()
中,當 mode Context.MODE_MULTI_PROCESS 時,會呼叫下面這個方法來重新從檔案中讀取資料
void startReloadIfChangedUnexpectedly() { synchronized (mLock) { if (!hasFileChangedUnexpectedly()) { return; } startLoadFromDisk(); } } //還有一處呼叫是在 SharedPreferencesImpl 的建構函式中 private void startLoadFromDisk() { synchronized (mLock) { mLoaded = false; } new Thread("SharedPreferencesImpl-load") { public void run() { loadFromDisk(); } }.start(); }
因此,這裡主要是強制在每次呼叫getSharedPreferences()
時都去從檔案中重新載入一邊,保證此時記憶體中的資料是最新的。
總結
在原始碼中可以發現官方已經將Context.MODE_MULTI_PROCESS
標記為棄用,而且極力推薦使用ContentProvider
來進行程序間的資料共享,因此使用這個 mode 來在程序下使用 SharedPreference 是不安全的,但有時我們只是需要儲存一些簡單的資料,用ContentProvider
好像又有點過於繁瑣了,所以我覺得,也需要視情況而定,如果你能保證自己的資料量不大,且使用不是很頻繁,那麼使用這個 mode 也不失為一個辦法。另外,在多程序下想安全的像使用 SharedPreference 來儲存和讀取資料,不妨試試騰訊開源的ofollow,noindex">MMKV
。