Android源碼進階之深入理解SharedPreference原理機制
前言
很久沒有分析源碼了,今天我們來分析下SharedPreferences;
大家一起來學(xué)習(xí);
一、SharedPreferences簡單使用
1、創(chuàng)建
第一個參數(shù)是儲存的xml文件名稱,第二個是打開方式,一般就用
- Context.MODE_PRIVATE;
- SharedPreferences sp=context.getSharedPreferences("名稱", Context.MODE_PRIVATE);
2、寫入
- //可以創(chuàng)建一個新的SharedPreference來對儲存的文件進行操作
- SharedPreferences sp=context.getSharedPreferences("名稱", Context.MODE_PRIVATE);
- //像SharedPreference中寫入數(shù)據(jù)需要使用Editor
- SharedPreference.Editor editor = sp.edit();
- //類似鍵值對
- editor.putString("name", "string");
- editor.putInt("age", 0);
- editor.putBoolean("read", true);
- //editor.apply();
- editor.commit();
- apply和commit都是提交保存,區(qū)別在于apply是異步執(zhí)行的,不需要等待。不論刪除,修改,增加都必須調(diào)用apply或者commit提交保存;
- 關(guān)于更新:如果已經(jīng)插入的key已經(jīng)存在。那么將更新原來的key;
- 應(yīng)用程序一旦卸載,SharedPreference也會被刪除;
3、讀取
- SharedPreference sp=context.getSharedPreferences("名稱", Context.MODE_PRIVATE);
- //第一個參數(shù)是鍵名,第二個是默認值
- String name=sp.getString("name", "暫無");
- int age=sp.getInt("age", 0);
- boolean read=sp.getBoolean("isRead", false);
4、檢索
- SharedPreferences sp=context.getSharedPreferences("名稱", Context.MODE_PRIVATE);
- //檢查當(dāng)前鍵是否存在
- boolean isContains=sp.contains("key");
- //使用getAll可以返回所有可用的鍵值
- //Map<String,?> allMaps=sp.getAll();
5、刪除
當(dāng)我們要清除SharedPreferences中的數(shù)據(jù)的時候一定要先clear()、再commit(),不能直接刪除xml文件;
- SharedPreference sp=getSharedPreferences("名稱", Context.MODE_PRIVATE);
- SharedPrefence.Editor editor=sp.edit();
- editor.clear();
- editor.commit();
- getSharedPreference() 不會生成文件,這個大家都知道;
- 刪除掉文件后,再次執(zhí)行commit(),刪除的文件會重生,重生文件的數(shù)據(jù)和刪除之前的數(shù)據(jù)相同;
- 刪除掉文件后,程序在沒有完全退出停止運行的情況下,Preferences對象所存儲的內(nèi)容是不變的,雖然文件沒有了,但數(shù)據(jù)依然存在;程序完全退出停止之后,數(shù)據(jù)才會丟失;
- 清除SharedPreferences數(shù)據(jù)一定要執(zhí)行editor.clear(),editor.commit(),不能只是簡單的刪除文件,這也就是最后的結(jié)論,需要注意的地方
二、SharedPreferences源碼分析
1、創(chuàng)建
- SharedPreferences preferences = getSharedPreferences("test", Context.MODE_PRIVATE);
實際上context的真正實現(xiàn)類是ContextImp,所以進入到ContextImp的getSharedPreferences方法查看:
- @Override
- public SharedPreferences getSharedPreferences(String name, int mode) {
- ......
- File file;
- synchronized (ContextImpl.class) {
- if (mSharedPrefsPaths == null) {
- //定義類型:ArrayMap<String, File> mSharedPrefsPaths;
- mSharedPrefsPaths = new ArrayMap<>();
- }
- //從mSharedPrefsPaths中是否能夠得到file文件
- file = mSharedPrefsPaths.get(name);
- if (file == null) {//如果文件為null
- //就創(chuàng)建file文件
- file = getSharedPreferencesPath(name);
- 將name,file鍵值對存入集合中
- mSharedPrefsPaths.put(name, file);
- }
- }
- return getSharedPreferences(file, mode);
- }
ArrayMap<String, File> mSharedPrefsPaths;對象是用來存儲SharedPreference文件名稱和對應(yīng)的路徑,獲取路徑是在下列方法中,就是獲取data/data/包名/shared_prefs/目錄下的
- @Override
- public File getSharedPreferencesPath(String name) {
- return makeFilename(getPreferencesDir(), name + ".xml");
- }
- private File getPreferencesDir() {
- synchronized (mSync) {
- if (mPreferencesDir == null) {
- mPreferencesDir = new File(getDataDir(), "shared_prefs");
- }
- return ensurePrivateDirExists(mPreferencesDir);
- }
- }
路徑之后才開始創(chuàng)建對象
- @Override
- public SharedPreferences getSharedPreferences(File file, int mode) {
- //重點1
- checkMode(mode);
- .......
- SharedPreferencesImpl sp;
- synchronized (ContextImpl.class) {
- //獲取緩存對象(或者創(chuàng)建緩存對象)
- final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
- //通過鍵file從緩存對象中獲取Sp對象
- sp = cache.get(file);
- //如果是null,就說明緩存中還沒后該文件的sp對象
- if (sp == null) {
- //重點2:從磁盤讀取文件
- sp = new SharedPreferencesImpl(file, mode);
- //添加到內(nèi)存中
- cache.put(file, sp);
- //返回sp
- return sp;
- }
- }
- //如果設(shè)置為MODE_MULTI_PROCESS模式,那么將執(zhí)行SP的startReloadIfChangedUnexpectedly方法。
- if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
- getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
- sp.startReloadIfChangedUnexpectedly();
- }
- return sp;
- }
就是重載之前的方法,只是入?yún)⒂晌募臑镕ile了,給創(chuàng)建過程加鎖了synchronized ,通過方法getSharedPreferencesCacheLocked()獲取系統(tǒng)中存儲的所有包名以及對應(yīng)的文件,這就是每個sp文件只有一個對應(yīng)的SharedPreferencesImpl實現(xiàn)對象原因
流程:
- 獲取緩存區(qū),從緩存區(qū)中獲取數(shù)據(jù),看是否存在sp對象,如果存在就直接返回
- 如果不存在,那么就從磁盤獲取數(shù)據(jù),
- 從磁盤獲取的數(shù)據(jù)之后,添加到內(nèi)存中,
- 返回sp;
getSharedPreferencesCacheLocked
- private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
- if (sSharedPrefsCache == null) {
- sSharedPrefsCache = new ArrayMap<>();
- }
- final String packageName = getPackageName();
- ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
- if (packagePrefs == null) {
- packagePrefs = new ArrayMap<>();
- sSharedPrefsCache.put(packageName, packagePrefs);
- }
- return packagePrefs;
- }
- getSharedPreferences(File file, int mode)方法中,從上面的系統(tǒng)緩存中分局File獲取SharedPreferencesImpl對象,如果之前沒有使用過,就需要創(chuàng)建一個對象了,通過方法checkMode(mode);
- 先檢查mode是否是三種模式,然后通過sp = new SharedPreferencesImpl(file, mode);
- 創(chuàng)建對象,并將創(chuàng)建的對象放到系統(tǒng)的packagePrefs中,方便以后直接獲取;
- SharedPreferencesImpl(File file, int mode) {
- mFile = file; //存儲文件
- //備份文件(災(zāi)備文件)
- mBackupFile = makeBackupFile(file);
- //模式
- mMode = mode;
- //是否加載過了
- mLoaded = false;
- // 存儲文件內(nèi)的鍵值對信息
- mMap = null;
- //從名字可以知道是:開始加載數(shù)據(jù)從磁盤
- startLoadFromDisk();
- }
- 主要是設(shè)置了幾個參數(shù),mFile 是原始文件;mBackupFile 是后綴.bak的備份文件;
- mLoaded標(biāo)識是否正在加載修改文件;
- mMap用來存儲sp文件中的數(shù)據(jù),存儲時候也是鍵值對形式,獲取時候也是通過這個獲取,這就是表示每次使用sp的時候,都是將數(shù)據(jù)寫入內(nèi)存,也就是sp數(shù)據(jù)存儲數(shù)據(jù)快的原因,所以sp文件不能存儲大量數(shù)據(jù),否則執(zhí)行時候很容易會導(dǎo)致OOM;
- mThrowable加載文件時候報的錯誤;
- 下面就是加載數(shù)據(jù)的方法startLoadFromDisk();從sp文件中加載數(shù)據(jù)到mMap中
2、startLoadFromDisk()
- private void startLoadFromDisk() {
- synchronized (mLock) {
- mLoaded = false;
- }
- //開啟子線程加載磁盤數(shù)據(jù)
- new Thread("SharedPreferencesImpl-load") {
- public void run() {
- loadFromDisk();
- }
- }.start();
- }
- private void loadFromDisk() {
- synchronized (mLock) {
- //如果加載過了 直接返回
- if (mLoaded) {
- return;
- }
- //備份文件是否存在,
- if (mBackupFile.exists()) {
- //刪除file原文件
- mFile.delete();
- //將備份文件命名為:xml文件
- mBackupFile.renameTo(mFile);
- }
- }
- .......
- Map map = null;
- StructStat stat = null;
- try {
- //下面的就是讀取數(shù)據(jù)
- stat = Os.stat(mFile.getPath());
- if (mFile.canRead()) {
- BufferedInputStream str = null;
- try {
- str = new BufferedInputStream(
- new FileInputStream(mFile), 16*1024);
- map = XmlUtils.readMapXml(str);
- } catch (Exception e) {
- Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
- } finally {
- IoUtils.closeQuietly(str);
- }
- }
- } catch (ErrnoException e) {
- /* ignore */
- }
- synchronized (mLock) {
- //已經(jīng)加載完畢,
- mLoaded = true;
- //數(shù)據(jù)不是null
- if (map != null) {
- //將map賦值給全局的存儲文件鍵值對的mMap對象
- mMap = map;
- //更新內(nèi)存的修改時間以及文件大小
- mStatTimestamp = stat.st_mtime;
- mStatSize = stat.st_size;
- } else {
- mMap = new HashMap<>();
- }
- //重點:喚醒所有以mLock鎖的等待線程
- mLock.notifyAll();
- }
- }
- 首先判斷備份文件是否存在,如果存在,就更該備份文件的后綴名;接著就開始讀取數(shù)據(jù),然后將讀取的數(shù)據(jù)賦值給全局變量存儲文件鍵值對的mMap對象,并且更新修改時間以及文件大小變量;
- 喚醒所有以mLock為鎖的等待線程;
- 到此為止,初始化SP對象就算完成了,其實可以看出來就是一個二級緩存流程:磁盤到內(nèi)存;
3、get獲取SP中的鍵值對
- @Nullable
- public String getString(String key, @Nullable String defValue) {
- synchronized (mLock) { 鎖判斷
- awaitLoadedLocked(); //等待機制
- String v = (String)mMap.get(key); //從鍵值對中獲取數(shù)據(jù)
- return v != null ? v : defValue;
- }
- }
- private void awaitLoadedLocked() {
- .......
- while (!mLoaded) { //在加載數(shù)據(jù)完畢的時候,值為true
- try {
- //線程等待
- mLock.wait();
- } catch (InterruptedException unused) {
- }
- }
- }
如果數(shù)據(jù)沒有加載完畢(也就是說mLoaded=false),此時將線程等待;
4、putXXX以及apply源碼
- public Editor edit() {
- //跟getXXX原理一樣
- synchronized (mLock) {
- awaitLoadedLocked();
- }
- //返回EditorImp對象
- return new EditorImpl();
- }
- public Editor putBoolean(String key, boolean value) {
- synchronized (mLock) {
- mModified.put(key, value);
- return this;
- }
- }
- public void apply() {
- final long startTime = System.currentTimeMillis();
- //根據(jù)名字可以知道:提交數(shù)據(jù)到內(nèi)存
- final MemoryCommitResult mcr = commitToMemory();
- ........
- //提交數(shù)據(jù)到磁盤中
- SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
- //重點:調(diào)用listener
- notifyListeners(mcr);
- }
- 先執(zhí)行了commitToMemory,提交數(shù)據(jù)到內(nèi)存;然后提交數(shù)據(jù)到磁盤中;
- 緊接著調(diào)用了listener;
5、commitToMemory
- private MemoryCommitResult commitToMemory() {
- long memoryStateGeneration;
- List<String> keysModified = null;
- Set<OnSharedPreferenceChangeListener> listeners = null;
- //寫到磁盤的數(shù)據(jù)集合
- Map<String, Object> mapToWriteToDisk;
- synchronized (SharedPreferencesImpl.this.mLock) {
- if (mDiskWritesInFlight > 0) {
- mMap = new HashMap<String, Object>(mMap);
- }
- //賦值此時緩存集合給mapToWriteToDisk
- mapToWriteToDisk = mMap;
- .......
- synchronized (mLock) {
- boolean changesMade = false;
- //重點:是否清空數(shù)據(jù)
- if (mClear) {
- if (!mMap.isEmpty()) {
- changesMade = true;
- //清空緩存中鍵值對信息
- mMap.clear();
- }
- mClear = false;
- }
- //循環(huán)mModified,將mModified中的數(shù)據(jù)更新到mMap中
- 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.
- 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);
- }
- .........
- }
- //清空臨時集合
- mModified.clear();
- ......
- }
- }
- return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
- mapToWriteToDisk);
- }
- mModified就是我們本次要更新添加的鍵值對集合;
- mClear是我們調(diào)用clear()方法的時候賦值的;
- 大致流程就是:首先判斷是否需要清空內(nèi)存數(shù)據(jù),然后循環(huán)mModified集合,添加更新數(shù)據(jù)到內(nèi)存的鍵值對集合中;
6、commit方法
- public boolean commit() {
- .......
- //更新數(shù)據(jù)到內(nèi)存
- MemoryCommitResult mcr = commitToMemory();
- //更新數(shù)據(jù)到磁盤
- SharedPreferencesImpl.this.enqueueDiskWrite(
- mcr, null /* sync write on this thread okay */);
- try {
- //等待:等待磁盤更新數(shù)據(jù)完成
- mcr.writtenToDiskLatch.await();
- } catch (InterruptedException e) {
- return false;
- } finally {
- if (DEBUG) {
- Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
- + " committed after " + (System.currentTimeMillis() - startTime)
- + " ms");
- }
- }
- //執(zhí)行l(wèi)istener回調(diào)
- notifyListeners(mcr);
- return mcr.writeToDiskResult;
- }
- 首先apply沒有返回值,commit有返回值;
- 其實apply執(zhí)行回調(diào)是和數(shù)據(jù)寫入磁盤并行執(zhí)行的,而commit方法執(zhí)行回調(diào)是等待磁盤寫入數(shù)據(jù)完成之后;
三、QueuedWork詳解
1、QueuedWork
QueuedWork這個類,因為sp的初始化之后就是使用,前面看到,無論是apply還是commit方法都是通過QueuedWork來實現(xiàn)的;
QueuedWork是一個管理類,顧名思義,其中有一個隊列,對所有入隊的work進行管理調(diào)度;
其中最重要的就是有一個HandlerThread
- private static Handler getHandler() {
- synchronized (sLock) {
- if (sHandler == null) {
- HandlerThread handlerThread = new HandlerThread("queued-work-looper",
- Process.THREAD_PRIORITY_FOREGROUND);
- handlerThread.start();
- sHandler = new QueuedWorkHandler(handlerThread.getLooper());
- }
- return sHandler;
- }
- }
2、入隊queue
- // 如果是commit,則不能delay,如果是apply,則可以delay
- public static void queue(Runnable work, boolean shouldDelay) {
- Handler handler = getHandler();
- synchronized (sLock) {
- sWork.add(work);
- if (shouldDelay && sCanDelay) {
- // 默認delay的時間是100ms
- handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
- } else {
- handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
- }
- }
- }
3、消息的處理
- private static class QueuedWorkHandler extends Handler {
- static final int MSG_RUN = 1;
- QueuedWorkHandler(Looper looper) {
- super(looper);
- }
- public void handleMessage(Message msg) {
- if (msg.what == MSG_RUN) {
- processPendingWork();
- }
- }
- }
- private static void processPendingWork() {
- synchronized (sProcessingWork) {
- LinkedList<Runnable> work;
- synchronized (sLock) {
- work = (LinkedList<Runnable>) sWork.clone();
- sWork.clear();
- getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
- }
- if (work.size() > 0) {
- for (Runnable w : work) {
- w.run();
- }
- }
- }
- }
- 可以看到,調(diào)度非常簡單,內(nèi)部有一個sWork,需要執(zhí)行的時候遍歷所有的runnable執(zhí)行;
- 對于apply操作,會有一定的延遲再去執(zhí)行work,但是對于commit操作,則會馬上觸發(fā)調(diào)度,而且并不僅僅是調(diào)度commit傳過來的那個任務(wù),而是馬上就調(diào)度隊列中所有的work;
4、waitToFinish
系統(tǒng)中很多地方會等待sp的寫入文件完成,等待方式是通過調(diào)用QueuedWork.waitToFinish();
- public static void waitToFinish() {
- Handler handler = getHandler();
- synchronized (sLock) {
- // 移除所有消息,直接開始調(diào)度所有work
- if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
- handler.removeMessages(QueuedWorkHandler.MSG_RUN);
- }
- sCanDelay = false;
- }
- StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
- try {
- // 如果是waitToFinish調(diào)用過來,則馬上執(zhí)行所有的work
- processPendingWork();
- } finally {
- StrictMode.setThreadPolicy(oldPolicy);
- }
- try {
- // 在所有的work執(zhí)行完畢之后,還需要執(zhí)行Finisher
- // 前面在apply的時候有一步是QueuedWork.addFinisher(awaitCommit);
- // 其中的實現(xiàn)是等待sp文件的寫入完成
- // 如果沒有通過msg去調(diào)度而是通過waitToFinish,則那個runnable就會在這里被執(zhí)行
- while (true) {
- Runnable finisher;
- synchronized (sLock) {
- finisher = sFinishers.poll();
- }
- if (finisher == null) {
- break;
- }
- finisher.run();
- }
- } finally {
- sCanDelay = true;
- }
- ...
- }
系統(tǒng)中對于四大組件的處理邏輯都在ActivityThread中實現(xiàn),在service/activity的生命周期的執(zhí)行中都會等待sp的寫入完成,正是通過調(diào)用QueuedWork.waitToFinish(),確保app的數(shù)據(jù)正確的寫入到disk;
5、sp使用的建議
- 對數(shù)據(jù)實時性要求不高,盡量使用apply
- 如果業(yè)務(wù)要求必須數(shù)據(jù)成功寫入,使用commit
- 減少sp操作頻次,盡量一次commit把所有的數(shù)據(jù)都寫入完畢
- 可以適當(dāng)考慮不要在主線程訪問sp
- 寫入sp的數(shù)據(jù)盡量輕量級
總結(jié):
SharedPreferences的本身實現(xiàn)就是分為兩步,一步是內(nèi)存,一部是磁盤,而主線程又依賴SharedPreferences的寫入,所以可能當(dāng)io成為瓶頸的時候,App會因為SharedPreferences變的卡頓,嚴重情況下會ANR,總結(jié)下來有以下幾點:
- 存放在xml文件中的數(shù)據(jù)會被裝在到內(nèi)存中,所以獲取數(shù)據(jù)很快
- apply是異步操作,提交數(shù)據(jù)到內(nèi)存,并不會馬上提交到磁盤
- commit是同步操作,會等待數(shù)據(jù)寫入到磁盤,并返回結(jié)果
- 如果有同一個線程多次commit,則后面的要等待前面執(zhí)行結(jié)束
- 如果多個線程對同一個sp并發(fā)commit,后面的所有任務(wù)會進入到QueuedWork中排隊執(zhí)行,且都要等第一個執(zhí)行完畢
本文轉(zhuǎn)載自微信公眾號「Android開發(fā)編程」
【編輯推薦】