因爲在本身練手的App項目中使用了SharedPreferences技術,因此對其進行必定探究。java
首先咱們先總結一下,Android的數據持久化方式:SharedPrefences、SQLite、文件儲存、ContentProvider、網絡儲存。其他四種,留後再進一步探究。android
老規矩看看類註釋是怎麼介紹的
緩存
由Context.getSharedPreferences方法放回的,能夠訪問和修改參數數據的接口。對於任一一組數據,都有一個全部客戶機共享的該類實例。對參數數據的修改,必須經過Editor對象,該對象確保了參數數值的一致性和控制客戶端將數值提交到儲存。由各類get方法獲得的對象必須是不可變得對象。
該類保證了強一致性,可是不支持跨進程使用。安全
從這段註釋中,咱們不難發現:SharedPreferences須要經過Context建立,該類與Editor對象密切相關,在應用內能夠數據共享。網絡
咱們在SharedPreferences類中往下尋找,就找到Editor接口併發
/** * Interface used for modifying values in a {@link SharedPreferences} * object. All changes you make in an editor are batched, and not copied * back to the original {@link SharedPreferences} until you call {@link #commit} * or {@link #apply} */ public interface Editor { Editor putString(String key, @Nullable String value); Editor putLong(String key, long value); Editor putFloat(String key, float value); Editor putBoolean(String key, boolean value); Editor remove(String key); Editor clear(); boolean commit(); void apply(); }
從這個Editor接口中,咱們能夠獲得幾個信息。首先SharedPreferences只能儲存4類數據,String,Long,Float,Boolean;其次SharedPreferences是使用key-value鍵值對的方式進行儲存的;最後,有兩種提交方式apply(),commit()。app
apply()和commit()的區別:異步
ContextImpl.getSharePreferences() 該類在AS上不顯示。根據當前應用名稱獲取ArrayMap(存儲sp容器),並根據文件名獲取SharedPreferencesImpl對象(實現SharedPreferences接口)。
SharedPreferences
對象,也就是說,屢次調用getSharedPreferences
方法並不會對性能形成多大影響,由於又緩存機制對象的建立過程是線程安全的,由於使用了
synchronize`關鍵字mode
使用了Context.MODE_MULTI_PROCESS
,那麼將會調用sp.startReloadIfChangedUnexpectedly()
方法,在startReloadIfChangedUnexpectedly
方法中,會判斷是否由其餘進程修改過這個文件,若是有,會從新從磁盤中讀取文件加載數據class ContextImpl extends Context { //靜態存儲類,緩存全部應用的SP容器,該容器key對應應用名稱,value則爲每一個應用存儲全部sp的容器 private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache; @Override public SharedPreferences getSharedPreferences(String name, int mode) { ...... // 根據 名字獲取相對應的文件名。 // 若是沒有則直接新建一個 File file; 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); } @Override public SharedPreferences getSharedPreferences(File file, int mode) { SharedPreferencesImpl sp; synchronized (ContextImpl.class) { // 從ArrayMap中獲取到應用儲存的value final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked(); // 從當前的Map中獲取一個,若是沒有則直接新建一個而且放回 sp = cache.get(file); if (sp == null) { 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 " + "storage are not available until after user is unlocked"); } } 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) { // 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的對象實例 sharedPreferencesImpl類
用SP存儲的靜態變量鍵值數據在內存中是一直存在(文件存儲),經過SharedPreferencesImpl構造器開啓一個線程對文件進行讀取。SharedPreferencesImpl主要是對文件進行操做。ide
file
以及mode
分別保存在mFile
以及mMode
中.bak
備份文件,當用戶寫入失敗的時候會根據這個備份文件進行恢復工做mMap
初始化爲null
startLoadFromDisk()
方法加載數據startLoadFromDisk()
getSharedPreferences
方法的時候,會從磁盤中加載數據,而數據的加載時經過開啓一個子線程調用loadFromDisk
方法進行異步讀取的mMap
中mStatTimestamp
以及mStatSize
中(保存這兩個值有什麼用呢?咱們在分析getSharedPreferences
方法時說過,若是有其餘進程修改了文件,而且mode
爲MODE_MULTI_PROCESS
,將會判斷從新加載文件。如何判斷文件是否被其餘進程修改過,沒錯,根據文件修改時間以及文件大小便可知道)notifyAll()
方法通知喚醒其餘等待線程,數據已經加載完畢SharedPreferencesImpl(File file, int mode) { mFile = file; // 文件備份 mBackupFile = makeBackupFile(file); mMode = mode; mLoaded = false; mMap = null; mThrowable = null; // 開啓一個線程讀取文件 startLoadFromDisk(); } private void startLoadFromDisk() { synchronized (mLock) { mLoaded = false; } new Thread("SharedPreferencesImpl-load") { public void run() { loadFromDisk(); } }.start(); } private void loadFromDisk() { synchronized (mLock) { //若是文件已經加載完畢直接返回 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<String, Object> map = null; StructStat stat = null; Throwable thrown = null; try { stat = Os.stat(mFile.getPath()); if (mFile.canRead()) { //讀取文件 BufferedInputStream str = null; try { str = new BufferedInputStream( new FileInputStream(mFile), 16 * 1024); //使用XmlUtils工具類讀取xml文件數據 map = (Map<String, Object>) XmlUtils.readMapXml(str); } catch (Exception e) { Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e); } finally { IoUtils.closeQuietly(str); } } } catch (ErrnoException e) { // An errno exception means the stat failed. Treat as empty/non-existing by // ignoring. } catch (Throwable t) { thrown = t; } synchronized (mLock) { //修改文件加載完成標誌 mLoaded = true; mThrowable = thrown; // It's important that we always signal waiters, even if we'll make // them fail with an exception. The try-finally is pretty wide, but // better safe than sorry. try { if (thrown == null) { if (map != null) { mMap = map;/若是有數據,將數據已經賦值給類成員變量mMap(將從文件讀取的數據賦值給mMap) mStatTimestamp = stat.st_mtim; mStatSize = stat.st_size; } else { //沒有數據直接建立一個hashmap對象 mMap = new HashMap<>(); } } // In case of a thrown exception, we retain the old map. That allows // any open editors to commit and store updates. } catch (Throwable t) { mThrowable = t; } finally { //此處很是關鍵是爲了通知其餘線程文件已讀取完畢,大家能夠執行讀/寫操做了 mLock.notifyAll(); } } }
sharedPreferencesImpl重寫了SharedPreferences的方法,基本上結構都一致,這裏拿String類舉例。工具
getXxx
方法是線程安全的,由於使用了synchronize
關鍵字getXxx
方法是直接操做內存的,直接從內存中的mMap
中根據傳入的key
讀取value
getXxx
方法有可能會卡在awaitLoadedLocked
方法,從而致使線程阻塞等待(何時會出現這種阻塞現象呢?前面咱們分析過,第一次調用getSharedPreferences
方法時,會建立一個線程去異步加載數據,那麼假如在調用完getSharedPreferences
方法以後當即調用getXxx
方法,此時的mLoaded
頗有可能爲false
,這就會致使awaiteLoadedLocked
方法阻塞等待,直到loadFromDisk
方法加載完數據而且調用notifyAll
來喚醒全部等待線程)public String getString(String key, @Nullable String defValue) { synchronized (mLock) { //此處會阻塞當前線程,直到文件加載完畢,第一次使用的時候可能會阻塞主線程 awaitLoadedLocked(); //從類成員變量mMap中直接讀取數據,沒有直接返回默認值 String v = (String)mMap.get(key); return v != null ? v : defValue; } }
3.1 獲取editor對象
public Editor edit() { synchronized (mLock) { awaitLoadedLocked();//若是文件未加載完畢,會一直阻塞當前線程,直到加載完成爲止 } return new EditorImpl(); }
3.2 對數據進行修改
SharedPreferences
的寫操做是線程安全的,由於使用了synchronize
關鍵字mModified
中,而並非直接對SharedPreferences.mMap
進行操做(mModified
會在commit/apply
方法中起到同步內存SharedPreferences.mMap
以及磁盤數據的做用)public final class EditorImpl implements Editor { //先存儲在Editor的map中 private final Map<String, Object> mModified = new HashMap<>(); //各類修改方法依舊相似 public Editor putString(String key, @Nullable String value) { synchronized (mEditorLock) { mModified.put(key, value); return this; } } ... }
3.3 提交數據
public boolean commit() { long startTime = 0; if (DEBUG) { startTime = System.currentTimeMillis(); } //第一步 commitToMemory方法能夠理解爲對SP中的mMap對象同步到最新數據狀態 //mcr對象就是最終須要寫入磁盤的mMap MemoryCommitResult mcr = commitToMemory(); //第二步 寫文件;注意第二個參數爲null,寫文件操做會運行在當前線程 //當前只有一個commit線程時。會直接在當前線程執行 //若是是UI線程 則可能會形成阻塞 //會判斷有無 備份文件,必定要有備份文件,防止寫入錯誤 //將mcr寫入磁盤 SharedPreferencesImpl.this.enqueueDiskWrite( mcr, null /* sync write on this thread okay */); try { mcr.writtenToDiskLatch.await(); } catch (InterruptedException e) { return false; } finally { if (DEBUG) { Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration + " committed after " + (System.currentTimeMillis() - startTime) + " ms"); } } //第三步 通知監聽器數據改變 notifyListeners(mcr); //第四步 返回寫操做狀態 return mcr.writeToDiskResult; }
public void apply() { final long startTime = System.currentTimeMillis(); final MemoryCommitResult mcr = commitToMemory(); final Runnable awaitCommit = new Runnable() { @Override public void run() { try { mcr.writtenToDiskLatch.await(); } catch (InterruptedException ignored) { } if (DEBUG && mcr.wasWritten) { Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration + " applied after " + (System.currentTimeMillis() - startTime) + " ms"); } } }; QueuedWork.addFinisher(awaitCommit); Runnable postWriteRunnable = new Runnable() { @Override public void run() { awaitCommit.run(); QueuedWork.removeFinisher(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); }
SharedPreferences 不要存放特別大的數據
apply
或者commit
,都會把所有的數據一次性寫入磁盤, 因此 SP 文件不該該過大, 影響總體性能SharedPreference
的文件存儲性能與文件大小相關,咱們不要將毫無關聯的配置項保存在同一個文件中,同時考慮將頻繁修改的條目單獨隔離出來getXxx
都是從內存中取的數據,數據來源於SharedPreferences.mMap
apply
同步回寫(commitToMemory()
)內存SharedPreferences.mMap
,而後把異步回寫磁盤的任務放到一個單線程的線程池隊列中等待調度。apply
不須要等待寫入磁盤完成,而是立刻返回ommit
同步回寫(commitToMemory()
)內存SharedPreferences.mMap
,而後若是mDiskWritesInFlight
(此時須要將數據寫入磁盤,但還未處理或未處理完成的次數)的值等於1,那麼直接在調用commit
的線程執行回寫磁盤的操做,不然把異步回寫磁盤的任務放到一個單線程的線程池隊列中等待調度。commit
會阻塞調用線程,知道寫入磁盤完成才返回MODE_MULTI_PROCESS
是在每次getSharedPreferences
時檢查磁盤上配置文件上次修改時間和文件大小,一旦全部修改則會從新從磁盤加載文件,因此並不能保證多進程數據的實時同步屢次edit屢次commit/apply