Android SharedPreferences 實現原理解析
序言
Android 中的 SharedPreference 是輕量級的資料儲存方式,能夠儲存簡單的資料型別,比如 String、int、boolean 值等。其內部是以 XML 結構儲存在 /data/data/包名/shared_prefs 資料夾下,資料以鍵值對的形式儲存。下面有個例子:
<?xml version='1.0' encoding='utf-8' standalone='yes' ?> <map> <float name="isFloat" value="1.5" /> <string name="isString">Android</string> <int name="isInt" value="1" /> <long name="isLong" value="1000" /> <boolean name="isBoolean" value="true" /> <set name="isStringSet"> <string>element 1</string> <string>element 2</string> <string>element 3</string> </set> </map>
這裡不討論 API 的使用方法,主要是從原始碼角度分析 SharedPreferences (以下簡稱 SP) 的實現方式。
1. 初始化
首先我們使用 context 的 getSharedPreferences 方法獲取 SP 例項,它是一個介面物件。
SharedPreferences testSp = getSharedPreferences("test_sp", Context.MODE_PRIVATE);
Context 是一個抽象類,其核心實現類是 ContextImpl ,找到裡面的 getSharedPreferences 方法。
@Override public SharedPreferences getSharedPreferences(String name, int mode) { SharedPreferencesImpl sp; synchronized (ContextImpl.class) { if (sSharedPrefs == null) { // sSharedPrefs 是 ContextImpl 的靜態成員變數,通過 Map 維護著當前包名下的 SP Map 集合 sSharedPrefs = new ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>>(); } final String packageName = getPackageName(); ArrayMap<String, SharedPreferencesImpl> packagePrefs = sSharedPrefs.get(packageName); if (packagePrefs == null) { packagePrefs = new ArrayMap<String, SharedPreferencesImpl>(); sSharedPrefs.put(packageName, packagePrefs); } // 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) { // name 引數為 null 時,檔名使用 null.xml if (name == null) { name = "null"; } } sp = packagePrefs.get(name); if (sp == null) { // SP 集合是一個以 SP 的名字為 key , SP 為值的 Map File prefsFile = getSharedPrefsFile(name); // SP 的實現類是 SharedPreferencesImpl sp = new SharedPreferencesImpl(prefsFile, mode); packagePrefs.put(name, sp); return sp; } } // Android 3.0 以下或者支援 MODE_MULTI_PROCESS 模式時,如果檔案被改動,就重新從檔案讀取,實現多程序資料同步,但是實際使用中效果不佳,可能會有很多坑。 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; }
首次使用 getSharedPreferences 時,記憶體中不存在 SP 以及 SP Map 快取,需要建立 SP 並新增到 ContextImpl 的靜態成員變數(sSharedPrefs)中。
下面來看 SharedPreferencesImpl 的構造方法,
SharedPreferencesImpl(File file, int mode) { mFile = file; mBackupFile = makeBackupFile(file); mMode = mode; mLoaded = false; mMap = null; startLoadFromDisk(); }
makeBackupFile 用來定義備份檔案,該檔案在寫入磁碟時會用到,繼續看 startLoadFromDisk 方法。
private void startLoadFromDisk() { synchronized (this) { mLoaded = false; } // 開啟非同步執行緒從磁碟讀取檔案,加鎖防止多執行緒併發操作 new Thread("SharedPreferencesImpl-load") { public void run() { synchronized (SharedPreferencesImpl.this) { loadFromDiskLocked(); } } }.start(); } private void loadFromDiskLocked() { 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,內部使用了 XmlPullParser map = XmlUtils.readMapXml(str); } catch (XmlPullParserException e) { Log.w(TAG, "getSharedPreferences", e); } catch (FileNotFoundException e) { Log.w(TAG, "getSharedPreferences", e); } catch (IOException e) { Log.w(TAG, "getSharedPreferences", e); } finally { IoUtils.closeQuietly(str); } } } catch (ErrnoException e) { } mLoaded = true; if (map != null) { mMap = map; mStatTimestamp = stat.st_mtime; mStatSize = stat.st_size; } else { mMap = new HashMap<String, Object>(); } // 喚醒等待的執行緒,到這檔案讀取完畢 notifyAll(); }
看到這,基本明白了 getSharedPreferences 的原理,應用首次使用 SP 的時候會從磁碟讀取,之後快取在記憶體中。
2. 讀資料
下面分析 SP 讀取資料的方法,就以 getString 為例。
@Nullable public String getString(String key, @Nullable String defValue) { synchronized (this) { awaitLoadedLocked(); String v = (String)mMap.get(key); return v != null ? v : defValue; } } 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 { wait(); } catch (InterruptedException unused) { } } }
首先取得 SharedPreferencesImpl 物件鎖,然後同步等待從磁碟載入資料完成,最後返回資料。這裡有個問題,如果單個 SP 儲存的內容過多,導致我們使用 getXXX 方法的時候阻塞,特別是在主執行緒呼叫的時候,所以建議在單個 SP 中儘量少地儲存資料,雖然操作時間是毫秒級別的,使用者基本上感覺不到。
3. 寫資料
SP 寫入資料的操作是通過 Editor 完成的,它也是一個介面,實現類是 EditorImpl,是 SharedPreferencesImpl 的內部類。
通過 SP 的 edit 方法獲取 Editor 例項,等到載入完畢直接返回一個 EditorImpl 物件。
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 (this) { awaitLoadedLocked(); } return new EditorImpl(); }
比如我們要儲存某個 String 的值,呼叫 putString 方法。
public Editor putString(String key, @Nullable String value) { synchronized (this) { mModified.put(key, value); return this; } }
mModified 是一個 editor 中的一個 Map,儲存著要修改的資料,在將改動儲存到 SP 的 Map(變數 mMap,裡面儲存著使用中的鍵值對 ) 後被清空。put 完成後就要呼叫 commit 或者 apply 進行儲存。
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; } 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); }
可以看到,commit 和 apply 操作首先執行了 commitToMemory,顧名思義就是提交到記憶體,返回值是 MemoryCommitResult 型別,裡面儲存著本次提交的狀態。然後 commit 呼叫 enqueueDiskWrite 會阻塞當前執行緒,而 apply 通過封裝 Runnable 把寫磁碟之後的操作傳遞給 enqueueDiskWrite 方法。
private MemoryCommitResult commitToMemory() { MemoryCommitResult mcr = new MemoryCommitResult(); synchronized (SharedPreferencesImpl.this) { // We optimistically don't make a deep copy until // a memory commit comes in when we're already // writing to disk. // mDiskWritesInFlight表示準備操作磁碟的程序數 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); } mcr.mapToWriteToDisk = mMap; mDiskWritesInFlight++; //把註冊的 listeners 放到 mcr 中去,以便在資料寫入的時候被回撥 boolean hasListeners = mListeners.size() > 0; if (hasListeners) { mcr.keysModified = new ArrayList<String>(); mcr.listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet()); } synchronized (this) { if (mClear) { if (!mMap.isEmpty()) { mcr.changesMade = true; mMap.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. // 當值是 null 時,表示移除該鍵值對,在 editor 的 remove 實現中,並不是真正地移除, // 而是把 value 賦值為當前 editor 物件 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); } mcr.changesMade = true; if (hasListeners) { mcr.keysModified.add(k); } } // 新增完成後把 editor 裡的 map 清空 mModified.clear(); } } return mcr; }
這是 MemoryCommitResult 類,主要用於提交到記憶體後返回結果,然後在寫入磁碟時作為引數傳遞。
private static class MemoryCommitResult { public boolean changesMade;// any keys different? public List<String> keysModified;// may be null public Set<OnSharedPreferenceChangeListener> listeners;// may be null public Map<?, ?> mapToWriteToDisk; public final CountDownLatch writtenToDiskLatch = new CountDownLatch(1); public volatile boolean writeToDiskResult = false; public void setDiskWriteResult(boolean result) { writeToDiskResult = result; writtenToDiskLatch.countDown(); } }
下面看儲存到磁碟的操作,enqueueDiskWrite 方法,引數有 MemoryCommitResult 和 Runnable,mcr 剛才說過,就看這個 Runnable 是幹嘛的。在 commit 方法中呼叫 enqueueDiskWrite 方法是傳入的 Runnable 是null,它會在當前執行緒直接執行寫檔案的操作,然後返回寫入結果。而如果 Runnable 不是 null,那就使用 QueueWork 中的單執行緒執行。這就是 apply 和 commit 的根本區別:一個同步執行,有返回值;一個非同步執行,沒有返回值。大多數情況下,我們使用 apply 就夠了,這也是官方推薦的做法。
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 方法,關鍵點在程式碼中文註釋部分。簡單說就是備份 → 寫入 → 檢查 → 善後,這樣保證了資料的安全性和穩定性。
private void writeToFile(MemoryCommitResult mcr) { // Rename the current file so it may be used as a backup during the next read if (mFile.exists()) { if (!mcr.changesMade) { // If the file already exists, but no changes were // made to the underlying map, it's wasteful to // re-write the file.Return as if we wrote it // out. mcr.setDiskWriteResult(true); return; } // 首先把當前的檔案備份 if (!mBackupFile.exists()) { if (!mFile.renameTo(mBackupFile)) { Log.e(TAG, "Couldn't rename file " + mFile + " to backup file " + mBackupFile); mcr.setDiskWriteResult(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 (str == null) { mcr.setDiskWriteResult(false); return; } // 然後把新資料寫入檔案 XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str); FileUtils.sync(str); str.close(); ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0); try { final StructStat stat = Os.stat(mFile.getPath()); synchronized (this) { mStatTimestamp = stat.st_mtime; mStatSize = stat.st_size; } } catch (ErrnoException e) { // Do nothing } // Writing was successful, delete the backup file if there is one. // 寫入成功刪除備份檔案 mBackupFile.delete(); mcr.setDiskWriteResult(true); 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); }
4. 總結
通過 getSharedPreferences 可以獲取 SP 例項,從首次初始化到讀到資料會存在延遲,因為讀檔案的操作阻塞呼叫的執行緒直到檔案讀取完畢,如果在主執行緒呼叫,可能會對 UI 流暢度造成影響。
commit 會在呼叫者執行緒同步執行寫檔案,返回寫入結果;apply 將寫檔案的操作非同步執行,沒有返回值。可以根據具體情況選擇性使用,推薦使用 apply。
雖然支援設定 MODE_MULTI_PROCESS 標誌位,但是跨程序共享 SP 存在很多問題,所以不建議使用該模式。
【附錄】

資料圖
需要資料的朋友可以加入Android架構交流QQ群聊:513088520
點選連結加入群聊【Android移動架構總群】: 加入群聊
獲取免費學習視訊,學習大綱另外還有像高階UI、效能優化、架構師課程、NDK、混合式開發(ReactNative+Weex)等Android高階開發資料免費分享。