原始碼進階之深入理解SharedPreference原理機制

語言: CN / TW / HK

theme: cyanosis

小知識,大挑戰!本文正在參與「程式設計師必備小知識」創作活動


本文已參與 「掘力星計劃」 ,贏取創作大禮包,挑戰創作激勵金

前言

很久沒有分析原始碼了,今天我們來分析下SharedPreferences;

大家一起來學習;

Android進階之徹底理解LruCache快取機制原理

Java執行緒進階之ThreadPoolExecutor執行緒池執行原理機制詳解

Android原始碼進階之Glide快取機制原理詳解

Java執行緒進階之ThreadPoolExecutor執行緒池執行原理機制詳解

Android架構進階之深入理解AppStartup原理

Android原始碼進階之ViewDragHelper原理機制解析

Android原始碼進階之深入理解Retrofit工作原理

前端進階之JS執行原理和機制詳解

原始碼進階之lifecycle元件原理分析

一、SharedPreferences簡單使用

1、建立

第一個引數是儲存的xml檔名稱,第二個是開啟方式,一般就用Context.MODE_PRIVATE;

java SharedPreferences sp=context.getSharedPreferences("名稱", Context.MODE_PRIVATE); 2、寫入 ```java //可以建立一個新的SharedPreference來對儲存的檔案進行操作

SharedPreferences sp=context.getSharedPreferences("名稱", Context.MODE_PRIVATE);

//像SharedPreference中寫入資料需要使用Editor

SharedPreference.Editor editor = sp.edit();

//類似鍵值對

editor.putString("name", "string");

editor.putInt("age", 0);

editor.putBoolean("read", true);

//editor.apply();

editor.commit(); ``` - apply和commit都是提交儲存,區別在於apply是非同步執行的,不需要等待。不論刪除,修改,增加都必須呼叫apply或者commit提交儲存; - 關於更新:如果已經插入的key已經存在。那麼將更新原來的key; - 應用程式一旦解除安裝,SharedPreference也會被刪除;

3、讀取 ```java SharedPreference sp=context.getSharedPreferences("名稱", Context.MODE_PRIVATE);

//第一個引數是鍵名,第二個是預設值

String name=sp.getString("name", "暫無");

int age=sp.getInt("age", 0);

boolean read=sp.getBoolean("isRead", false); **4、檢索**java SharedPreferences sp=context.getSharedPreferences("名稱", Context.MODE_PRIVATE);

//檢查當前鍵是否存在

boolean isContains=sp.contains("key");

//使用getAll可以返回所有可用的鍵值

//Map allMaps=sp.getAll(); ``` 5、刪除

當我們要清除SharedPreferences中的資料的時候一定要先clear()、再commit(),不能直接刪除xml檔案; ```java SharedPreference sp=getSharedPreferences("名稱", Context.MODE_PRIVATE);

SharedPrefence.Editor editor=sp.edit();

editor.clear();

editor.commit(); ``` - getSharedPreference() 不會生成檔案,這個大家都知道; - 刪除掉檔案後,再次執行commit(),刪除的檔案會重生,重生檔案的資料和刪除之前的資料相同; - 刪除掉檔案後,程式在沒有完全退出停止執行的情況下,Preferences物件所儲存的內容是不變的,雖然檔案沒有了,但資料依然存在;程式完全退出停止之後,資料才會丟失; - 清除SharedPreferences資料一定要執行editor.clear(),editor.commit(),不能只是簡單的刪除檔案,這也就是最後的結論,需要注意的地方

二、SharedPreferences原始碼分析

image.png

1、建立

SharedPreferences preferences = getSharedPreferences("test", Context.MODE_PRIVATE);

實際上context的真正實現類是ContextImp,所以進入到ContextImp的getSharedPreferences方法檢視: ```java     @Override

public SharedPreferences getSharedPreferences(String name, int mode) {

......

File file;

synchronized (ContextImpl.class) {

if (mSharedPrefsPaths == null) {

//定義型別:ArrayMap mSharedPrefsPaths;

mSharedPrefsPaths = new ArrayMap<>();

}

//從mSharedPrefsPaths中是否能夠得到file檔案

file = mSharedPrefsPaths.get(name);

if (file == null) {//如果檔案為null

//就建立file檔案

file = getSharedPreferencesPath(name);

將name,file鍵值對存入集合中

mSharedPrefsPaths.put(name, file);

}

}

return getSharedPreferences(file, mode);

} ArrayMap<String, File> mSharedPrefsPaths;物件是用來儲存SharedPreference檔名稱和對應的路徑,獲取路徑是在下列方法中,就是獲取data/data/包名/shared_prefs/目錄下的java @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);

}

} **路徑之後才開始建立物件**java    @Override

public SharedPreferences getSharedPreferences(File file, int mode) {

//重點1

checkMode(mode);

.......

SharedPreferencesImpl sp;

synchronized (ContextImpl.class) {

//獲取快取物件(或者建立快取物件)

final ArrayMap cache = getSharedPreferencesCacheLocked();

//通過鍵file從快取物件中獲取Sp物件

sp = cache.get(file);

//如果是null,就說明快取中還沒後該檔案的sp物件

if (sp == null) {

//重點2:從磁碟讀取檔案

sp = new SharedPreferencesImpl(file, mode);

//新增到記憶體中

cache.put(file, sp);

//返回sp

return sp;

}

}

//如果設定為MODE_MULTI_PROCESS模式,那麼將執行SP的startReloadIfChangedUnexpectedly方法。

if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||

getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {

sp.startReloadIfChangedUnexpectedly();

}

return sp;

} ```

就是過載之前的方法,只是入參由檔名改為File了,給建立過程加鎖了synchronized ,通過方法getSharedPreferencesCacheLocked()獲取系統中儲存的所有包名以及對應的檔案,這就是每個sp檔案只有一個對應的SharedPreferencesImpl實現物件原因

流程:

  • 獲取快取區,從快取區中獲取資料,看是否存在sp物件,如果存在就直接返回
  • 如果不存在,那麼就從磁盤獲取資料,
  • 從磁盤獲取的資料之後,新增到記憶體中,
  • 返回sp;

getSharedPreferencesCacheLocked ```java private ArrayMap getSharedPreferencesCacheLocked() {

if (sSharedPrefsCache == null) {

sSharedPrefsCache = new ArrayMap<>();

}

final String packageName = getPackageName();

ArrayMap packagePrefs = sSharedPrefsCache.get(packageName);

if (packagePrefs == null) {

packagePrefs = new ArrayMap<>();

sSharedPrefsCache.put(packageName, packagePrefs);

}

return packagePrefs;

} - getSharedPreferences(File file, int mode)方法中,從上面的系統快取中分局File獲取SharedPreferencesImpl物件,如果之前沒有使用過,就需要建立一個物件了,通過方法checkMode(mode); - 先檢查mode是否是三種模式,然後通過sp = new SharedPreferencesImpl(file, mode); - 建立物件,並將建立的物件放到系統的packagePrefs中,方便以後直接獲取;java SharedPreferencesImpl(File file, int mode) {

mFile = file; //儲存檔案

//備份檔案(災備檔案)

mBackupFile = makeBackupFile(file);

//模式

mMode = mode;

//是否載入過了

mLoaded = false;

// 儲存檔案內的鍵值對資訊

mMap = null;

//從名字可以知道是:開始載入資料從磁碟

startLoadFromDisk();

} ``` - 主要是設定了幾個引數,mFile 是原始檔案;mBackupFile 是字尾.bak的備份檔案; - mLoaded標識是否正在載入修改檔案; - mMap用來儲存sp檔案中的資料,儲存時候也是鍵值對形式,獲取時候也是通過這個獲取,這就是表示每次使用sp的時候,都是將資料寫入記憶體,也就是sp資料儲存資料快的原因,所以sp檔案不能儲存大量資料,否則執行時候很容易會導致OOM; - mThrowable載入檔案時候報的錯誤; - 下面就是載入資料的方法startLoadFromDisk();從sp檔案中載入資料到mMap中

image.png

2、startLoadFromDisk() ```java  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()) {

//刪除file原檔案

mFile.delete();

//將備份檔案命名為:xml檔案

mBackupFile.renameTo(mFile);

}

}

.......

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);

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) {

//已經載入完畢,

mLoaded = true;

//資料不是null

if (map != null) {

//將map賦值給全域性的儲存檔案鍵值對的mMap物件

mMap = map;

//更新記憶體的修改時間以及檔案大小

mStatTimestamp = stat.st_mtime;

mStatSize = stat.st_size;

} else {

mMap = new HashMap<>();

}

//重點:喚醒所有以mLock鎖的等待執行緒

mLock.notifyAll();

}

} ``` - 首先判斷備份檔案是否存在,如果存在,就更該備份檔案的字尾名;接著就開始讀取資料,然後將讀取的資料賦值給全域性變數儲存檔案鍵值對的mMap物件,並且更新修改時間以及檔案大小變數; - 喚醒所有以mLock為鎖的等待執行緒; - 到此為止,初始化SP物件就算完成了,其實可以看出來就是一個二級快取流程:磁碟到記憶體;

3、get獲取SP中的鍵值對 ```java  @Nullable

public String getString(String key, @Nullable String defValue) {

synchronized (mLock) { 鎖判斷

awaitLoadedLocked(); //等待機制

String v = (String)mMap.get(key); //從鍵值對中獲取資料

return v != null ? v : defValue;

}

}

private void awaitLoadedLocked() {

.......

while (!mLoaded) { //在載入資料完畢的時候,值為true

try {

//執行緒等待

mLock.wait();

} catch (InterruptedException unused) {

}

}

} ``` 如果資料沒有載入完畢(也就是說mLoaded=false),此時將執行緒等待;

4、putXXX以及apply原始碼 ```java 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();

//根據名字可以知道:提交資料到記憶體

final MemoryCommitResult mcr = commitToMemory();

........

//提交資料到磁碟中

SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

//重點:呼叫listener

notifyListeners(mcr);

} ``` - 先執行了commitToMemory,提交資料到記憶體;然後提交資料到磁碟中; - 緊接著呼叫了listener;

5、commitToMemory ```java         // Returns true if any changes were made

private MemoryCommitResult commitToMemory() {

long memoryStateGeneration;

List keysModified = null;

Set listeners = null;

//寫到磁碟的資料集合

Map mapToWriteToDisk;

synchronized (SharedPreferencesImpl.this.mLock) {

if (mDiskWritesInFlight > 0) {

mMap = new HashMap(mMap);

}

//賦值此時快取集合給mapToWriteToDisk 

mapToWriteToDisk = mMap;

.......

synchronized (mLock) {

boolean changesMade = false;

//重點:是否清空資料

if (mClear) {

if (!mMap.isEmpty()) {

changesMade = true;

//清空快取中鍵值對資訊

mMap.clear();

}

mClear = false;

}

//迴圈mModified,將mModified中的資料更新到mMap中

for (Map.Entry 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是我們呼叫clear()方法的時候賦值的; - 大致流程就是:首先判斷是否需要清空記憶體資料,然後迴圈mModified集合,新增更新資料到記憶體的鍵值對集合中;

6、commit方法 ```java  public boolean commit() {

.......

//更新資料到記憶體

MemoryCommitResult mcr = commitToMemory();

//更新資料到磁碟

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");

}

}

//執行listener回撥

notifyListeners(mcr);

return mcr.writeToDiskResult;

} ``` - 首先apply沒有返回值,commit有返回值; - 其實apply執行回撥是和資料寫入磁碟並行執行的,而commit方法執行回撥是等待磁碟寫入資料完成之後;

二、QueuedWork詳解

1、QueuedWork

QueuedWork這個類,因為sp的初始化之後就是使用,前面看到,無論是apply還是commit方法都是通過QueuedWork來實現的;

QueuedWork是一個管理類,顧名思義,其中有一個佇列,對所有入隊的work進行管理排程;

其中最重要的就是有一個HandlerThread ```java  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**java     // 如果是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、訊息的處理**java     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 work;

synchronized (sLock) {

work = (LinkedList) sWork.clone();

sWork.clear();

getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);

}

if (work.size() > 0) {

for (Runnable w : work) {

w.run();

}

}

}

} ``` - 可以看到,排程非常簡單,內部有一個sWork,需要執行的時候遍歷所有的runnable執行; - 對於apply操作,會有一定的延遲再去執行work,但是對於commit操作,則會馬上觸發排程,而且並不僅僅是排程commit傳過來的那個任務,而是馬上就排程佇列中所有的work;

4、waitToFinish

系統中很多地方會等待sp的寫入檔案完成,等待方式是通過呼叫QueuedWork.waitToFinish(); ```java     public static void waitToFinish() {

Handler handler = getHandler();

synchronized (sLock) {

// 移除所有訊息,直接開始排程所有work

if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {

handler.removeMessages(QueuedWorkHandler.MSG_RUN);

}

sCanDelay = false;

}

StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();

try {

// 如果是waitToFinish呼叫過來,則馬上執行所有的work

processPendingWork();

} finally {

StrictMode.setThreadPolicy(oldPolicy);

}

try {

// 在所有的work執行完畢之後,還需要執行Finisher

// 前面在apply的時候有一步是QueuedWork.addFinisher(awaitCommit);

// 其中的實現是等待sp檔案的寫入完成

// 如果沒有通過msg去排程而是通過waitToFinish,則那個runnable就會在這裡被執行

while (true) {

Runnable finisher;

synchronized (sLock) {

finisher = sFinishers.poll();

}

if (finisher == null) {

break;

}

finisher.run();

}

} finally {

sCanDelay = true;

}

...

} ``` 系統中對於四大元件的處理邏輯都在ActivityThread中實現,在service/activity的生命週期的執行中都會等待sp的寫入完成,正是通過呼叫QueuedWork.waitToFinish(),確保app的資料正確的寫入到disk;

5、sp使用的建議

  • 對資料實時性要求不高,儘量使用apply
  • 如果業務要求必須資料成功寫入,使用commit
  • 減少sp操作頻次,儘量一次commit把所有的資料都寫入完畢
  • 可以適當考慮不要在主執行緒訪問sp
  • 寫入sp的資料儘量輕量級

總結:

SharedPreferences的本身實現就是分為兩步,一步是記憶體,一部是磁碟,而主執行緒又依賴SharedPreferences的寫入,所以可能當io成為瓶頸的時候,App會因為SharedPreferences變的卡頓,嚴重情況下會ANR,總結下來有以下幾點:

  • 存放在xml檔案中的資料會被裝在到記憶體中,所以獲取資料很快
  • apply是非同步操作,提交資料到記憶體,並不會馬上提交到磁碟
  • commit是同步操作,會等待資料寫入到磁碟,並返回結果
  • 如果有同一個執行緒多次commit,則後面的要等待前面執行結束
  • 如果多個執行緒對同一個sp併發commit,後面的所有任務會進入到QueuedWork中排隊執行,且都要等第一個執行完畢