在Android的日常開發中,相信大家都用過SharedPreferences來保存用戶的某些settings值。Shared Preferences
以鍵值對的形式存儲私有的原生類型數據,這里的私有的是指只對你自己的app可見的,也就是說別的app是無法訪問到的。
客戶端代碼為了使用它有2種方式,一種是通過Context#getSharedPreferences(String prefName, int mode)方法,
另一種是Activity自己的getPreferences(int mode)方法,其內部還是調用了前者只是用activity的類名做了prefName而已,
我們先來看下Conext#getSharedPreferences的內部實現。其具體實現在ContextImpl.java文件中,代碼如下:
@Override public SharedPreferences getSharedPreferences(String name, int mode) { SharedPreferencesImpl sp; // 這個是我們接下來要分析的重點類 synchronized (ContextImpl.class) { if (sSharedPrefs == null) { // sSharedPrefs是一個靜態的ArrayMap,注意這個類型,表示一個包可以對應有一組SharedPreferences sSharedPrefs = new ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>>(); } // ArrayMap<String, SharedPreferencesImpl>表示文件名到SharedpreferencesImpl的映射關系 final String packageName = getPackageName(); // 先通過包名找到與之關聯的prefs集合packagePrefs ArrayMap<String, SharedPreferencesImpl> packagePrefs = sSharedPrefs.get(packageName); if (packagePrefs == null) { // lazy initialize packagePrefs = new ArrayMap<String, SharedPreferencesImpl>(); sSharedPrefs.put(packageName, packagePrefs); // 添加到全局sSharedPrefs中 } // 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"; // name傳null時候的特殊處理,用"null"代替 } } sp = packagePrefs.get(name); // 再找與文件名name關聯的sp對象; if (sp == null) { // 如果還沒有, File prefsFile = getSharedPrefsFile(name); // 則先根據name構建一個prefsFile對象 sp = new SharedPreferencesImpl(prefsFile, mode); // 再new一個SharedPreferencesImpl對象的實例 packagePrefs.put(name, sp); // 並添加到packagePrefs中
return sp; // 第一次直接return } }
// 如果不是第一次,則在Android3.0之前或者mode設置成了MULTI_PROCESS的話,調用reload 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; // 最后返回SharedPreferences的具體對象sp }
通過分析這段代碼我們大體能得到2個重要結論:
1. 靜態的ArrayMap變量sSharedPrefs,因為它一直伴隨我們的app存在,所以如果你的SharedPreferences很多的話,map會很大,
從而會占用較大部分內存;一般來說,你可以將多個小的prefs文件合並到一個稍大的里面。
2. 當你用SharedPreferences來跨進程通信的時候,你會發現你不能像往常(非MODE_MULTI_PROCESS的情況)那樣,調用一次
getSharedPreferences方法然后用這個實例來讀取值。因為如果你不是每次調用getSharedPreferences方法的話,此方法最后的那段
reload代碼不會被執行,那么可能別的進程寫的最新數據在你的進程里面還是看不到(本人項目親歷)。而且reload雖然不在UI線程中操
作但畢竟也是耗時(費力)的IO操作,所以Android doc關於Context.MODE_MULTI_PROCESS字段的說明中也明確提及有更好的跨進
程通信方式。
看SharedPreferences的源碼我們知道它只是一個接口而已,在其內部又有2個嵌套的接口:OnSharedPreferenceChangeListener
和Editor;前者代表了回調接口,表示當一個shared preference改變時如果你感興趣則有能力收聽到通知;Editor則定義了用來寫值的
接口,而用來讀數據的方法都在大的SharedPreferences接口中定義。它們的具體實現在SharedPreferencesImpl.java文件中。
下面就讓我們睜大眼睛,好好研究下這個類具體是怎么實現的。和以往一樣,我們還是從關鍵字段和ctor開始,源碼如下:
// Lock ordering rules: // 這3行注釋明確寫明了加鎖的順序,注意下;在我們自己的代碼里如果遇到類似 // - acquire SharedPreferencesImpl.this before EditorImpl.this // (需要多把鎖)的情況,則最好也寫清楚, // - acquire mWritingToDiskLock before EditorImpl.this // 這是個很好的習慣,方便別人看你的代碼。 private final File mFile; // 我們的shared preferences背后存儲在這個文件里 private final File mBackupFile; // 與mFile對應的備份文件 private final int mMode; // 如MODE_PRIVATE,MODE_WORLD_READABLE,MODE_WORLD_WRITEABLE,MODE_MULTI_PROCESS等 private Map<String, Object> mMap; // guarded by 'this' 將settings緩存在內存中的map private int mDiskWritesInFlight = 0; // guarded by 'this' 表示還未寫到disk中的寫操作的數目 private boolean mLoaded = false; // guarded by 'this' 表示settings整個從disk加載到內存map中完畢的標志 private long mStatTimestamp; // guarded by 'this' 文件的最近一次更新時間 private long mStatSize; // guarded by 'this' 文件的size,注意這些字段都被this對象保護 private final Object mWritingToDiskLock = new Object(); // 寫操作的鎖對象
接着我們看看其構造器:
SharedPreferencesImpl(File file, int mode) { mFile = file; mBackupFile = makeBackupFile(file); // 根據file,產生一個.bak的File對象 mMode = mode; mLoaded = false; mMap = null; startLoadFromDisk(); }
構造器也比較簡單,主要做2件事情,初始化重要變量&將文件異步加載到內存中。
下面我們緊接着看下將settings文件異步加載到內存中的操作:
private void startLoadFromDisk() { synchronized (this) { mLoaded = false; // 開始load前,將其reset(加鎖),后面的loadFromDiskLocked方法會檢測這個標記 } new Thread("SharedPreferencesImpl-load") { public void run() { synchronized (SharedPreferencesImpl.this) { loadFromDiskLocked(); // 在一個新的線程中開始load,注意鎖加在SharedPreferencesImpl對象上, } // 也就是說這時候如果其他線程調用SharedPreferences.getXXX之類的方法都會被阻塞。 } }.start(); } private void loadFromDiskLocked() { // 此方法受SharedPreferencesImpl.this鎖的保護 if (mLoaded) { // 如果已加載完畢則直接返回 return; } if (mBackupFile.exists()) { mFile.delete(); // 如果備份文件存在,則刪除(非備份)文件mFile, mBackupFile.renameTo(mFile); // 將備份文件重命名為mFile(相當於mFile現在又存在了只是內容其實已經變成了mBackupFile而已) } // 或者說接下來的讀操作實際是從備份文件中來的 // 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 = Libcore.os.stat(mFile.getPath()); // 得到文件的一系列信息,有linux c經驗的同學應該都很眼熟 if (mFile.canRead()) { // 前提是文件可讀啊。。。一般都是成立的,否則我們最終會得到一個空的map BufferedInputStream str = null; try { str = new BufferedInputStream( new FileInputStream(mFile), 16*1024); map = XmlUtils.readMapXml(str); // 用str中所有xml信息構造一個map返回 } 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; // 如果map非空,則設置mMap,並更新文件訪問時間、文件大小字段 mStatTimestamp = stat.st_mtime; mStatSize = stat.st_size; } else { mMap = new HashMap<String, Object>(); // 否則初始化一個empty的map } notifyAll(); // 最后通知所有阻塞在SharedPreferencesImpl.this對象上的線程數據ready了,可以往下進行了 }
接下來我們看看將文件reload進內存的方法:
void startReloadIfChangedUnexpectedly() { synchronized (this) { // 也是在SharedPreferencesImpl.this對象上加鎖 // TODO: wait for any pending writes to disk? if (!hasFileChangedUnexpectedly()) { // 如果沒有我們之外的意外更改,則直接返回,因為我們的數據 return; // 仍然是最新的,沒必要reload } startLoadFromDisk(); // 真正需要reload } } // Has the file changed out from under us? i.e. writes that // we didn't instigate. private boolean hasFileChangedUnexpectedly() { // 這個方法檢測是否別的進程也修改了文件 synchronized (this) { if (mDiskWritesInFlight > 0) { // 知道是我們自己引起的,則直接返回false,表示是預期的 // If we know we caused it, it's not unexpected. if (DEBUG) Log.d(TAG, "disk write in flight, not unexpected."); return false; } } final StructStat stat; try { /* * Metadata operations don't usually count as a block guard * violation, but we explicitly want this one. */ BlockGuard.getThreadPolicy().onReadFromDisk(); stat = Libcore.os.stat(mFile.getPath()); } catch (ErrnoException e) { return true; } synchronized (this) { // 比較文件的最近更新時間和size是否和我們手頭的一樣,如果不一樣則說明有unexpected修改 return mStatTimestamp != stat.st_mtime || mStatSize != stat.st_size; } }
接下來要分析的是一堆讀操作相關的,各種getXXX,它們做的事情本質都是一樣的,不一個個分析了,只說下大體思想:在同步塊中
等待加載完成,然后直接從mMap中返回需要的信息,而不是每次都觸發一次讀文件操作(本人沒看源碼之前一直以為是讀文件操作),
這里我們只看下block等待的方法:
private void awaitLoadedLocked() { // 注意此方法也是在SharedPreferencesImpl.this鎖的保護下 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) { // 當條件變量不成立時(即沒load完成)則無限等待 try { // 注意這個經典的形式我們已經見到好幾次了(上一次是在HandlerThread中,還記得?) wait(); } catch (InterruptedException unused) { } } }
接下來我們看看真正修改(寫)文件的操作是怎么實現的,代碼如下:
// Return value from EditorImpl#commitToMemory() private static class MemoryCommitResult { // 此靜態類表示EditorImpl#commitToMemory()的返回值 public boolean changesMade; // any keys different? public List<String> keysModified; // may be null public Set<OnSharedPreferenceChangeListener> listeners; // may be null public Map<?, ?> mapToWriteToDisk; // 要寫到disk中的map(持有數據的map) public final CountDownLatch writtenToDiskLatch = new CountDownLatch(1); // 初始化為1的count down閉鎖 public volatile boolean writeToDiskResult = false; public void setDiskWriteResult(boolean result) { // 結束寫操作的時候調用,result為true表示成功 writeToDiskResult = result; writtenToDiskLatch.countDown(); // 此調用會釋放所有block在await調用上的線程 } } public final class EditorImpl implements Editor { // Editor的具體實現類 private final Map<String, Object> mModified = Maps.newHashMap(); // 持有所有要修改的數據即調用putXXX方法時提供的參數 private boolean mClear = false; public Editor putString(String key, String value) { synchronized (this) { // EditorImpl.this鎖用來保護mModified對象 mModified.put(key, value); // 修改不是立即寫到文件中的,而是暫時放在內存的map中的 return this; // 返回當前對象,以便支持鏈式方法調用 } } public Editor putStringSet(String key, Set<String> values) { synchronized (this) { mModified.put(key, (values == null) ? null : new HashSet<String>(values)); return this; } } public Editor putInt(String key, int value) { synchronized (this) { mModified.put(key, value); return this; } } public Editor putLong(String key, long value) { synchronized (this) { mModified.put(key, value); return this; } } public Editor putFloat(String key, float value) { synchronized (this) { mModified.put(key, value); return this; } } public Editor putBoolean(String key, boolean value) { synchronized (this) { mModified.put(key, value); return this; } } public Editor remove(String key) { synchronized (this) { mModified.put(key, this); // 注意remove操作比較特殊,remove一個key時會put一個特殊的this對象, return this; // 后面的commitToMemory方法對此有特殊處理 } } public Editor clear() { synchronized (this) { mClear = true; return this; } } public void apply() { final MemoryCommitResult mcr = commitToMemory(); final Runnable awaitCommit = new Runnable() { public void run() { try { mcr.writtenToDiskLatch.await(); // block等待寫操作完成 } catch (InterruptedException ignored) { } } }; QueuedWork.add(awaitCommit); // 將awaitCommit添加到QueueWork中;這里順帶引出一個疑問:那么apply方法到底
// 會不會導致SharedPreferences丟失數據更新呢?(有興趣的同學可以看看QueuedWork#waitToFinish方法都在哪里,
// 什么情況下被調用了就明白了)
Runnable postWriteRunnable = new Runnable() { // 寫操作完成之后要執行的runnable public void run() { awaitCommit.run(); // 執行awaitCommit runnable並從QueueWork中移除 QueuedWork.remove(awaitCommit); } }; SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable); // 准備將mcr寫到磁盤中 // 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); } // Returns true if any changes were made private MemoryCommitResult commitToMemory() { // 當此方法調用時,這里有2級鎖,先是SharedPreferencesImpl.this鎖, MemoryCommitResult mcr = new MemoryCommitResult(); // 然后是EditorImpl.this鎖,所以當commit的時候任何調用getXXX synchronized (SharedPreferencesImpl.this) {// 的方法都會block。此方法的目的主要是構造一個合適的MemoryCommitResult對象。 // 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); // 當有多個寫操作等待執行時make a copy of mMap } mcr.mapToWriteToDisk = mMap; mDiskWritesInFlight++; // 表示又多了一個(未完成的)寫操作 boolean hasListeners = mListeners.size() > 0; if (hasListeners) { mcr.keysModified = new ArrayList<String>(); mcr.listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet()); } synchronized (this) { // 加鎖在EditorImpl對象上 if (mClear) { // 處理clear的情況 if (!mMap.isEmpty()) { mcr.changesMade = true; mMap.clear(); } mClear = false; // reset } // 注意這里由於先處理了clear操作,所以clear並不會清掉本次寫操作的數據,只會clear掉以前有的數據 for (Map.Entry<String, Object> e : mModified.entrySet()) { // 遍歷mModified處理各個key、value String k = e.getKey(); Object v = e.getValue(); if (v == this) { // magic value for a removal mutation // 這個就是標記為刪除的特殊value if (!mMap.containsKey(k)) { continue; } mMap.remove(k); // 從mMap中刪除 } else { boolean isSame = false; if (mMap.containsKey(k)) { Object existingValue = mMap.get(k); if (existingValue != null && existingValue.equals(v)) { continue; } } mMap.put(k, v); // 將mModified中的值更新到mMap中 } mcr.changesMade = true; // 走到這步表示有更新產生 if (hasListeners) { mcr.keysModified.add(k); } } mModified.clear(); // 一次commit執行完后清空mModified,准備接下來的put操作 } } return mcr; } public boolean commit() { MemoryCommitResult mcr = commitToMemory(); SharedPreferencesImpl.this.enqueueDiskWrite( // 發起寫操作 mcr, null /* sync write on this thread okay */); try { // block等待寫操作完成,如果是UI線程可能會造成UI卡頓,所以Android建議我們如果不關心返回值可以考慮用apply替代 mcr.writtenToDiskLatch.await(); } catch (InterruptedException e) { return false; } notifyListeners(mcr); return mcr.writeToDiskResult; } private void notifyListeners(final MemoryCommitResult mcr) { // 注意此方法中callback調用永遠發生在UI線程中 if (mcr.listeners == null || mcr.keysModified == null || mcr.keysModified.size() == 0) { return; } if (Looper.myLooper() == Looper.getMainLooper()) { for (int i = mcr.keysModified.size() - 1; i >= 0; i--) { final String key = mcr.keysModified.get(i); for (OnSharedPreferenceChangeListener listener : mcr.listeners) { if (listener != null) { listener.onSharedPreferenceChanged(SharedPreferencesImpl.this, key); } } } } else { // Run this function on the main thread. ActivityThread.sMainThreadHandler.post(new Runnable() { public void run() { notifyListeners(mcr); } }); } } }
最后我們看下SharedPreferencesImpl的最后3個重要方法(也即真正寫操作發生的地方):
/** * 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, // 此方法的doc寫的很詳細,你可以仔細閱讀下 final Runnable postWriteRunnable) { final Runnable writeToDiskRunnable = new Runnable() { // 真正寫操作的runnable public void run() { synchronized (mWritingToDiskLock) { // 第3把鎖,保護寫操作的 writeToFile(mcr); } synchronized (SharedPreferencesImpl.this) { mDiskWritesInFlight--; // 表示1個寫操作完成了,少了1個in flight的了 } if (postWriteRunnable != null) { postWriteRunnable.run(); // 如果非空則執行之(apply的時候滿足) } } }; final boolean isFromSyncCommit = (postWriteRunnable == null); // 判斷我們是否從commit方法來的 // Typical #commit() path with fewer allocations, doing a write on // the current thread. if (isFromSyncCommit) { boolean wasEmpty = false; synchronized (SharedPreferencesImpl.this) { wasEmpty = mDiskWritesInFlight == 1; // 如果mDiskWritesInFlight是1的話表示有1個寫操作需要執行 } if (wasEmpty) { // 在UI線程中直接調用其run方法執行之 writeToDiskRunnable.run(); return; // 執行完畢后返回 } } // 否則來自apply調用的話,直接扔一個writeToDiskRunnable給單線程的thread executor去執行 QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable); } // 依據file創建與之對應的文件(在文件系統中) private static FileOutputStream createFileOutputStream(File file) { FileOutputStream str = null; try { str = new FileOutputStream(file); } catch (FileNotFoundException e) { File parent = file.getParentFile(); if (!parent.mkdir()) { Log.e(TAG, "Couldn't create directory for SharedPreferences file " + file); return null; } FileUtils.setPermissions( parent.getPath(), FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH, -1, -1); try { str = new FileOutputStream(file); } catch (FileNotFoundException e2) { Log.e(TAG, "Couldn't create SharedPreferences file " + file, e2); } } return str; } // Note: must hold mWritingToDiskLock private void writeToFile(MemoryCommitResult mcr) { // Rename the current file so it may be used as a backup during the next read if (mFile.exists()) { // 如果對應的mFile存在的話,針對於非第一次操作 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()) { // 如果沒備份文件存在的話,嘗試將mFile重命名為mBackupFile
// 因為如果本次寫操作失敗的話(可能這時數據已經不完整了或破壞掉了),下次再讀的話還可以從備份文件中恢復 if (!mFile.renameTo(mBackupFile)) { // 如果重命名失敗則調用mcr.setDiskWriteResult(false)結束 Log.e(TAG, "Couldn't rename file " + mFile + " to backup file " + mBackupFile); mcr.setDiskWriteResult(false); return; } } else { // 備份文件存在的話,則刪除mFile(因為接下來我們馬上要重新寫一個新mFile了) 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); // 嘗試創建mFile if (str == null) { // 如果失敗則調用mcr.setDiskWriteResult(false)收場 mcr.setDiskWriteResult(false); return; } XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str); // 將mcr的mapToWriteToDisk全部寫到str對應的文件中 FileUtils.sync(str); // 將buffer中的數據都flush到底層設備中 str.close(); // 關閉文件流 ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0); // 設置文件權限根據mMode try { final StructStat stat = Libcore.os.stat(mFile.getPath()); synchronized (this) { mStatTimestamp = stat.st_mtime; // 同步更新文件相關的2個變量 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()) { // 如果以上寫操作出了任何異常則刪掉(內容)不完整的mFile;放心因為開始寫之前我們已經備份了,哈哈 if (!mFile.delete()) { Log.e(TAG, "Couldn't clean up partially-written file " + mFile); } } mcr.setDiskWriteResult(false); // 標記寫操作以失敗告終 }
到現在我們算是明白了mMode和文件權限的關系,為了更清晰直觀的展現,最后附上ContextImpl.setFilePermissionsFromMode的源碼:
static void setFilePermissionsFromMode(String name, int mode, int extraPermissions) { int perms = FileUtils.S_IRUSR|FileUtils.S_IWUSR // 我們可以看出默認創建的文件權限是user自己可讀可寫, |FileUtils.S_IRGRP|FileUtils.S_IWGRP // 同組可讀可寫 |extraPermissions; // 和其他附加的,一般給0表示沒附加的權限 if ((mode&MODE_WORLD_READABLE) != 0) { // 接下來我們看到只有MODE_WORLD_READABLE/MODE_WORLD_WRITEABLE有用 perms |= FileUtils.S_IROTH; // other可讀 } if ((mode&MODE_WORLD_WRITEABLE) != 0) { perms |= FileUtils.S_IWOTH; // other可寫 } if (DEBUG) { Log.i(TAG, "File " + name + ": mode=0x" + Integer.toHexString(mode) + ", perms=0x" + Integer.toHexString(perms)); } FileUtils.setPermissions(name, perms, -1, -1); }
通過以上分析我們可以看出每次調用commit()、apply()都會將整個settings全部寫到文件中,即使你只改動了一個setting。因為它是
基於全局的,而不是增量的,所以你的客戶端代碼中一定不要出現一個putXXX就緊跟着一個commit/apply,而是put完所有你要的改動,
最后調用一次commit/apply即可。至此Android提供的持久化primitive數據的機制SharedPreferences就已經完全分析完畢了。