01.Sp簡單介紹php
02.Sp初始化操做java
04.put和get方法源碼android
05.commit和applygit
sp做用說明github
分析sp包含那些內容面試
分析sp包含那些源碼segmentfault
代碼以下所示緩存
long startA = System.currentTimeMillis(); for (int i=0 ; i<200 ; i++){ SharedPreferences preferences = this.getSharedPreferences("testA", 0); SharedPreferences.Editor edit = preferences.edit(); edit.putString("yc"+i,"yangchong"+i); edit.commit(); } long endA = System.currentTimeMillis(); long a = endA - startA; Log.i("測試A","----"+a); long startB = System.currentTimeMillis(); SharedPreferences preferencesB = this.getSharedPreferences("testB", 0); SharedPreferences.Editor editB = preferencesB.edit(); for (int i=0 ; i<200 ; i++){ editB.putString("yc"+i,"yangchong"+i); } editB.commit(); long endB = System.currentTimeMillis(); long b = endB - startB; Log.i("測試B","----"+b); long startC = System.currentTimeMillis(); SharedPreferences.Editor editC = null; for (int i=0 ; i<200 ; i++){ SharedPreferences preferencesC = this.getSharedPreferences("testC", 0); if (editC==null){ editC = preferencesC.edit(); } editC.putString("yc"+i,"yangchong"+i); } editC.commit(); long endC = System.currentTimeMillis(); long c = endC - startC; Log.i("測試C","----"+c);
而後開始執行操做安全
而後看一下執行結果markdown
2019-08-30 15:08:16.982 3659-3659/com.cheoo.app I/測試A: ----105 2019-08-30 15:08:17.035 3659-3659/com.cheoo.app I/測試B: ----52 2019-08-30 15:08:17.069 3659-3659/com.cheoo.app I/測試C: ----34 2019-08-30 15:08:20.561 3659-3659/com.cheoo.app I/測試A: ----25 2019-08-30 15:08:20.562 3659-3659/com.cheoo.app I/測試B: ----1 2019-08-30 15:08:20.564 3659-3659/com.cheoo.app I/測試C: ----2
結果分析
而後看看裏面存儲值
<?xml version='1.0' encoding='utf-8' standalone='yes' ?> <map> <string name="yc110">yangchong110</string> <string name="yc111">yangchong111</string> <string name="yc118">yangchong118</string> <string name="yc119">yangchong119</string> <string name="yc116">yangchong116</string> <string name="yc117">yangchong117</string> <string name="yc114">yangchong114</string> <string name="yc115">yangchong115</string> <string name="yc112">yangchong112</string> <string name="yc113">yangchong113</string> <string name="yc121">yangchong121</string> <string name="yc122">yangchong122</string> <string name="yc120">yangchong120</string> <string name="yc129">yangchong129</string> <string name="yc127">yangchong127</string> <string name="yc128">yangchong128</string> <string name="yc125">yangchong125</string> <string name="yc126">yangchong126</string> <string name="yc123">yangchong123</string> <string name="yc124">yangchong124</string> <string name="yc1">yangchong1</string> <string name="yc109">yangchong109</string> <string name="yc0">yangchong0</string> <string name="yc3">yangchong3</string> </map>
代碼以下所示
long startA = System.currentTimeMillis(); for (int i=0 ; i<200 ; i++){ SharedPreferences preferences = activity.getSharedPreferences("testA", 0); SharedPreferences.Editor edit = preferences.edit(); edit.putString("yc"+i,"yangchong"+i); edit.apply(); } long endA = System.currentTimeMillis(); long a = endA - startA; Log.i("測試A","----"+a); long startB = System.currentTimeMillis(); SharedPreferences preferencesB = activity.getSharedPreferences("testB", 0); SharedPreferences.Editor editB = preferencesB.edit(); for (int i=0 ; i<200 ; i++){ editB.putString("yc"+i,"yangchong"+i); } editB.apply(); long endB = System.currentTimeMillis(); long b = endB - startB; Log.i("測試B","----"+b); long startC = System.currentTimeMillis(); SharedPreferences.Editor editC = null; for (int i=0 ; i<200 ; i++){ SharedPreferences preferencesC = activity.getSharedPreferences("testC", 0); if (editC==null){ editC = preferencesC.edit(); } editC.putString("yc"+i,"yangchong"+i); } editC.apply(); long endC = System.currentTimeMillis(); long c = endC - startC; Log.i("測試C","----"+c);
而後看一下執行結果
2019-08-30 15:17:07.341 5522-5522/com.cheoo.app I/測試A: ----54 2019-08-30 15:17:07.346 5522-5522/com.cheoo.app I/測試B: ----5 2019-08-30 15:17:07.352 5522-5522/com.cheoo.app I/測試C: ----6 2019-08-30 15:17:10.541 5522-5522/com.cheoo.app I/測試A: ----32 2019-08-30 15:17:10.542 5522-5522/com.cheoo.app I/測試B: ----1 2019-08-30 15:17:10.543 5522-5522/com.cheoo.app I/測試C: ----1
得出結論
能夠看出屢次執行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(); }
首先看ContextWrapper源碼
@Override public SharedPreferences getSharedPreferences(String name, int mode) { return mBase.getSharedPreferences(name, mode); }
而後看一下ContextImpl類
@Override 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) { // 建立一個對應路徑 /data/data/packageName/name 的 File 對象 file = getSharedPreferencesPath(name); mSharedPrefsPaths.put(name, file); } } // 這裏調用了 getSharedPreferences(File file, int mode) 方法 return getSharedPreferences(file, mode); }
而後接着看一下getSharedPreferences(file, mode)方法源碼
@Override public SharedPreferences getSharedPreferences(File file, int mode) { SharedPreferencesImpl sp; // 這裏使用了 synchronized 關鍵字,確保了 SharedPreferences 對象的構造是線程安全的 synchronized (ContextImpl.class) { // 獲取SharedPreferences 對象的緩存,並複製給 cache final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked(); // 以參數 file 做爲 key,獲取緩存對象 sp = cache.get(file); if (sp == null) { // 若是緩存中不存在 SharedPreferences 對象 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 "
} } // 構造一個 SharedPreferencesImpl 對象 sp = new SharedPreferencesImpl(file, mode); // 放入緩存 cache 中,方便下次直接從緩存中獲取 cache.put(file, sp); // 返回新構造的 SharedPreferencesImpl 對象 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. // 若是由其餘進程修改了這個 SharedPreferences 文件,咱們將會從新加載它 sp.startReloadIfChangedUnexpectedly(); } // 程序走到這裏,說明命中了緩存,SharedPreferences 已經建立,直接返回 return sp; } ```
這段源碼的流程仍是清晰易懂的,註釋已經說得很明白,這裏咱們總結一下這個方法的要點:
看SharedPreferencesImpl的構造方法,源碼以下所示
// SharedPreferencesImpl.java // 構造方法 SharedPreferencesImpl(File file, int mode) { mFile = file; // 建立災備文件,命名爲prefsFile.getPath() + ".bak" mBackupFile = makeBackupFile(file); mMode = mode; // mLoaded表明是否已經加載完數據 mLoaded = false; // 解析 xml 文件獲得的鍵值對就存放在mMap中 mMap = null; // 顧名思義,這個方法用於加載 mFile 這個磁盤上的 xml 文件 startLoadFromDisk(); } // 建立災備文件,用於當用戶寫入失敗的時候恢復數據 private static File makeBackupFile(File prefsFile) { return new File(prefsFile.getPath() + ".bak"); }
而後看一下調用startLoadFromDisk()方法加載數據
// SharedPreferencesImpl.java private void startLoadFromDisk() { synchronized (this) { mLoaded = false; } //注意:這裏咱們能夠看出,SharedPreferences 是經過開啓一個線程來異步加載數據的 new Thread("SharedPreferencesImpl-load") { public void run() { // 這個方法纔是真正負責從磁盤上讀取 xml 文件數據 loadFromDisk(); } }.start(); } private void loadFromDisk() { synchronized (SharedPreferencesImpl.this) { // 若是正在加載數據,直接返回 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 { // 讀取數據而且將數據解析爲jia str = new BufferedInputStream( new FileInputStream(mFile), *); map = XmlUtils.readMapXml(str); } catch (XmlPullParserException | IOException e) { Log.w(TAG, "getSharedPreferences", e); } finally { IoUtils.closeQuietly(str); } } } catch (ErrnoException e) { /* ignore */ } synchronized (SharedPreferencesImpl.this) { // 加載數據成功,設置 mLoaded 爲 true mLoaded = true; if (map != null) { // 將解析獲得的鍵值對數據賦值給 mMap mMap = map; // 將文件的修改時間戳保存到 mStatTimestamp 中 mStatTimestamp = stat.st_mtime; // 將文件的大小保存到 mStatSize 中 mStatSize = stat.st_size; } else { mMap = new HashMap<>(); } // 通知喚醒全部等待的線程 notifyAll(); } }
對startLoadFromDisk()方法進行了分析,有分析咱們能夠獲得如下幾點總結:
源碼方法以下所示
@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(); }
就以putString爲例分析源碼。經過sharedPreferences.edit()方法返回的SharedPreferences.Editor,全部咱們對SharedPreferences的寫操做都是基於這個Editor類的。在 Android 系統中,Editor是一個接口類,它的具體實現類是EditorImpl:
public final class EditorImpl implements Editor { // putXxx/remove/clear等寫操做方法都不是直接操做 mMap 的,而是將全部 // 的寫操做先記錄在 mModified 中,等到 commit/apply 方法被調用,纔會將 // 全部寫操做同步到 內存中的 mMap 以及磁盤中 private final Map<String, Object> mModified = Maps.newHashMap(); // private boolean mClear = false; public Editor putString(String key, @Nullable String value) { synchronized (this) { mModified.put(key, value); return this; } } ...... 其餘方法 ...... }
從EditorImpl類的源碼咱們能夠得出如下總結:
就以getString爲例分析源碼
@Nullable public String getString(String key, @Nullable String defValue) { // synchronize 關鍵字用於保證 getString 方法是線程安全的 synchronized (this) { // 方法 awaitLoadedLocked() 用於確保加載完數據並保存到 mMap 中才進行數據讀取 awaitLoadedLocked(); // 根據 key 從 mMap中獲取 value String v = (String)mMap.get(key); // 若是 value 不爲 null,返回 value,若是爲 null,返回默認值 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(); } // 前面咱們說過,mLoaded 表明數據是否已經加載完畢 while (!mLoaded) { try { // 等待數據加載完成以後才返回繼續執行代碼 wait(); } catch (InterruptedException unused) { } } }
getString方法代碼很簡單,其餘的例如getInt,getFloat方法也是同樣的原理,直接對這個疑問進行總結:
commit()方法分析
public boolean commit() { // 前面咱們分析 putXxx 的時候說過,寫操做的記錄是存放在 mModified 中的 // 在這裏,commitToMemory() 方法就負責將 mModified 保存的寫記錄同步到內存中的 mMap 中 // 而且返回一個 MemoryCommitResult 對象 MemoryCommitResult mcr = commitToMemory(); // enqueueDiskWrite 方法負責將數據落地到磁盤上 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; }
commit()方法的主體結構很清晰簡單:
接着來看一下它調用的commitToMemory()方法:
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. 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); } // 將 mMap 賦值給 mcr.mapToWriteToDisk,mcr.mapToWriteToDisk 指向的就是最終寫入磁盤的數據 mcr.mapToWriteToDisk = mMap; // mDiskWritesInFlight 表明的是「此時須要將數據寫入磁盤,但還未處理或未處理完成的次數」 // 將 mDiskWritesInFlight 自增1(這裏是惟一會增長 mDiskWritesInFlight 的地方) mDiskWritesInFlight++; boolean hasListeners = mListeners.size() > 0; if (hasListeners) { mcr.keysModified = new ArrayList<String>(); mcr.listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet()); } synchronized (this) { // 只有調用clear()方法,mClear才爲 true if (mClear) { if (!mMap.isEmpty()) { mcr.changesMade = true; // 當 mClear 爲 true,清空 mMap mMap.clear(); } mClear = false; } // 遍歷 mModified for (Map.Entry<String, Object> e : mModified.entrySet()) { String k = e.getKey(); // 獲取 key Object v = e.getValue(); // 獲取 value // 當 value 的值是 "this" 或者 null,將對應 key 的鍵值對數據從 mMap 中移除 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); } } // 將 mModified 同步到 mMap 以後,清空 mModified 歷史記錄 mModified.clear(); } } return mcr; }
commitToMemory()方法主要作了這幾件事:
對調用的enqueueDiskWrite方法進行分析:
private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) { // 建立一個 Runnable 對象,該對象負責寫磁盤操做 final Runnable writeToDiskRunnable = new Runnable() { public void run() { synchronized (mWritingToDiskLock) { // 顧名思義了,這就是最終經過文件操做將數據寫入磁盤的方法了 writeToFile(mcr); } synchronized (SharedPreferencesImpl.this) { // 寫入磁盤後,將 mDiskWritesInFlight 自減1,表明寫磁盤的需求減小一個 mDiskWritesInFlight--; } if (postWriteRunnable != null) { // 執行 postWriteRunnable(提示,在 apply 中,postWriteRunnable 纔不爲 null) postWriteRunnable.run(); } } }; // 若是傳進的參數 postWriteRunnable 爲 null,那麼 isFromSyncCommit 爲 true // 舒適提示:從上面的 commit() 方法源碼中,能夠看出調用 commit() 方法傳入的 postWriteRunnable 爲 null 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) { // 若是此時只有一個 commit 請求(注意,是 commit 請求,而不是 apply )未處理,那麼 wasEmpty 爲 true wasEmpty = mDiskWritesInFlight == 1; } if (wasEmpty) { // 當只有一個 commit 請求未處理,那麼無需開啓線程進行處理,直接在本線程執行 writeToDiskRunnable 便可 writeToDiskRunnable.run(); return; } } // 將 writeToDiskRunnable 方法線程池中執行 // 程序執行到這裏,有兩種可能: // 1. 調用的是 commit() 方法,而且當前只有一個 commit 請求未處理 // 2. 調用的是 apply() 方法 QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable); } 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
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 = Libcore.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); } ``` - writeToFile這個方法大體分爲三個過程: - 先把已存在的老的 SP 文件重命名(加「.bak」後綴),而後刪除老的 SP 文件,這至關於作了備份(災備) - 向mFile中一次性寫入全部鍵值對數據,即mcr.mapToWriteToDisk(這就是commitToMemory所說的保存了全部鍵值對數據的字段) 一次性寫入到磁盤。 - 若是寫入成功則刪除備份(災備)文件,同時記錄了此次同步的時間若是往磁盤寫入數據失敗,則刪除這個半成品的 SP 文件
apply()方法分析
public void apply() { // 將 mModified 保存的寫記錄同步到內存中的 mMap 中,而且返回一個 MemoryCommitResult 對象 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); } }; // 將數據落地到磁盤上,注意,傳入的 postWriteRunnable 參數不爲 null,因此在 // enqueueDiskWrite 方法中會開啓子線程異步將數據寫入到磁盤中 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()方法: