Android-带你从源码角度理解SharedPreferences存储原理
SP的特點以及基本使用方式
SharedPreferences因非常適合存儲較小鍵值集合數據且使用非常簡單的特點,而受到廣大程序員們熱愛。
SP使用非常簡單:
SP源碼分析
SP是如何讀取數據的
其實Context實現就是ContextImpl中,要想搞清楚SP是如何讀取數據的,第一步當然是要了解ContextImpl.getSharedPreferences方法是如何實現的
/*** Map from package name, to preference name, to cached preferences.*///緩存所有應用的SP容器,該容器key對應應用名稱,value則為每個應用存儲所有sp的容器(ArrayMap)private static ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>> sSharedPrefs;@Overridepublic SharedPreferences getSharedPreferences(String name, int mode) {SharedPreferencesImpl sp;synchronized (ContextImpl.class) {if (sSharedPrefs == null) {//如果靜態對象不存在,直接創建一個Map,以便后期用于保存spsSharedPrefs = new ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>>();}//獲取當前應用包名final String packageName = getPackageName();//從保存sp的容器中通過包名查找當前應用所有sp;每個app的所有sp都是保存在ArrayMap中,ArrayMap<String, SharedPreferencesImpl> packagePrefs = sSharedPrefs.get(packageName);if (packagePrefs == null) {//如果從sp容器沒找到保存當前應用sp的ArrayMap直接創建一個packagePrefs = new ArrayMap<String, SharedPreferencesImpl>();//將創建的對象保存到sp容器中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) {//如果targetSdk版本好小于19,且傳入的文件名為null的話,默認將文件名命名為"null"if (name == null) {name = "null";}}//從當前應用的sp容器中通過文件名去查找spsp = packagePrefs.get(name);if (sp == null) {//如果沒找到,直接創建一個文件名以name命名的xml文件File prefsFile = getSharedPrefsFile(name);//此處極為關鍵,該構造器是讀取文件操作sp = new SharedPreferencesImpl(prefsFile, mode);//將創建sp對象保存到當前應用sp容器中packagePrefs.put(name, 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.//如果讀取模式是跨線程或targetSdk版本小于11,再次重新load下文件而已sp.startReloadIfChangedUnexpectedly();}return sp;}從上面源碼可以看出,getSharedPreferences方法是先根據當前應用名稱來獲取一個ArrayMap(存儲sp的容器)如果沒有直接創建并保存到內存中,然后再根據文件名來獲取SharedPreferencesImpl的對象(沒找到則直接創建SharedPreferencesImpl),這短短的三十幾行代碼還是比較簡單的。
因為是靜態變量存儲鍵值數據的所以我們用SP存儲的數據在內存中是一直存在;所以我們盡量用一個文件來存在數據,以達到減少內存對象
大家可以看到getSharePreferences返回的對象類型其實是SharedPreferencesImpl類型,只不過該類實現了SharedPreferences接口而已,接下來我們先看看SharedPreferencesImpl構造器做了啥東東。
SharedPreferencesImpl.java
final class SharedPreferencesImpl implements SharedPreferences {private final File mFile;private final File mBackupFile;private final int mMode;private Map<String, Object> mMap; // guarded by 'this'private int mDiskWritesInFlight = 0; // guarded by 'this'//文件是否加載成功private boolean mLoaded = false; // guarded by 'this'//文件的時間以及大小private long mStatTimestamp; // guarded by 'this'private long mStatSize; // guarded by 'this'private final Object mWritingToDiskLock = new Object();private static final Object mContent = new Object();private final WeakHashMap<OnSharedPreferenceChangeListener, Object> mListeners =new WeakHashMap<OnSharedPreferenceChangeListener, Object>();SharedPreferencesImpl(File file, int mode) {//給類成員變量賦值mFile = file;mBackupFile = makeBackupFile(file);mMode = mode;mLoaded = false;mMap = null;//開啟一個線程讀取文件startLoadFromDisk();}private static File makeBackupFile(File prefsFile) {return new File(prefsFile.getPath() + ".bak");}private void startLoadFromDisk() {synchronized (this) {//使用同步代碼代碼塊,對mloader進行賦值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);}// Debuggingif (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);//使用XmlUtils工具類讀取xml文件數據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;//如果有數據,將數據已經賦值給類成員變量mMap,mStatTimestamp = stat.st_mtime;//記錄文件上次修改時間mStatSize = stat.st_size;//記錄文件大小(字節單位)} else {//沒有數據直接創建一個hashmap對象mMap = new HashMap<String, Object>();}//此處非常關鍵是為了通知其他線程文件已讀取完畢,你們可以執行讀/寫操作了notifyAll();}}通過上述分析我們不難發現調用getSharedPreferences方法就已經開啟一個線程去讀取文件了
我們再來看看SharedPreferencesImpl.getString內部是如何執行的
至此關于SP的讀操作以全部分析完畢,相信大家對SP讀操作有了更深入的認識了;下面我們繼續再看看SP的寫操作是怎么玩的
SP是如何寫入數據的
通常寫數據大體是這樣的
//寫操作 Context context = getActivity(); SharedPreferences sp = getSharedPreferences("fileName", Context.MODE_PRIVATE); sp.edit().putString("key", "value").commit();getSharedPreferences大家都很清楚了,我們現在edit方法到底做了什么
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(); }可以看到edit方法非常簡單,首先是通過同步代碼塊調用了awaitLoadedLocked方法,緊接著直接返回了一個EditorImpl實例對象,我們繼續追蹤看看EditorImpl類是put、commit方法
public final class EditorImpl implements Editor {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;}}public Editor remove(String key) {synchronized (this) {//注意此處并沒有執行刪除操作,而是將其對應key的value設置了當前this//commitToMemory方法中會對此做特殊處理mModified.put(key, this);return this;}}public Editor clear() {synchronized (this) {mClear = true;return this;}}public boolean commit() {//第一步 commitToMemory方法可以理解為對SP中的mMap對象同步到最新數據狀態MemoryCommitResult mcr = commitToMemory();//第二步 寫文件;注意第二個參數為null,寫文件操作會運行在當前線程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;}... }從上面可以看到,其實putXXX方法知識把鍵值數據先存放到內存中去了(mModified對象中);比較有意思到是remove、clear方法,remove方法會將要刪除的數據的value設置為EditorImpl自己(commitToMemory方法會對此做特殊處理);
clear方法也僅僅是設置一個標志位而已(commitToMemory方法中用到);
最關鍵的方法還是commit方法,我們可以看到其實commit方法主要分為四步,第一步將sp數據同步到最新狀態并返回mcr對象;第二步將mcr對象中數據寫入文件;第三步通知監聽器數據已發生改變;最后一步就是返回寫操作狀態是否成功
相信大家應該SP的讀寫有了更深刻的認識!
總結
以上是生活随笔為你收集整理的Android-带你从源码角度理解SharedPreferences存储原理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android组件化初探
- 下一篇: 迁移至Android3.0遇到一些问题