我們經(jīng)常用SharedPreferences用來(lái)存儲(chǔ)一些比較小的鍵值對(duì)集合,適合保存應(yīng)用的配置參數(shù), 我們將會(huì)帶著以下幾個(gè)問(wèn)題來(lái)分析SharedPreferences的源碼實(shí)現(xiàn):
- 數(shù)據(jù)是如何保存到磁盤(pán)的
- commit() 和apply()的區(qū)別
- 為什么會(huì)造成ANR
- SharedPreferences有哪些缺點(diǎn)
源碼分析
本文參照Android-26的源碼,并不介紹SharedPreferences的基礎(chǔ)使用,而是從源碼角度來(lái)分析它的原理
獲取SharedPreferences
我們通過(guò)以下方法來(lái)獲取SharedPreferences實(shí)例
- context.getSharedPreferences
- 在Activity中g(shù)etSharedPreferences
- PreferenceManager.getDefaultSharedPreferences
這三種方法最終都會(huì)調(diào)用到 ContextImpl.getSharedPreferences
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
//SharedPreferences對(duì)應(yīng)的xml文件,數(shù)據(jù)保存在其中
File file;
synchronized (ContextImpl.class) {
...//省略
file = mSharedPrefsPaths.get(name);
if (file == null) {
//如果沒(méi)有該name命名的文件,則新建一個(gè)并放入緩存
file = getSharedPreferencesPath(name);
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode);
}
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
...//省略
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
if (sp == null) {
sp = new SharedPreferencesImpl(file, mode);
cache.put(file, sp);
return sp;
}
}
//mode設(shè)置為多進(jìn)程模式時(shí)會(huì)檢測(cè)SP文件最后修改的時(shí)間和大小,如果文件被其他進(jìn)程改變時(shí),則會(huì)重新加載
if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
sp.startReloadIfChangedUnexpectedly();
}
return sp;
}
可以看到最終返回的是一個(gè)SharedPreferencesImpl對(duì)象,首先getSharedPreferencesCacheLocked()從一個(gè)靜態(tài)的ArrayMap中獲取SharedPreferences 緩存,如果有緩存中有SharedPreferencesImpl對(duì)象則返回,沒(méi)有的話則創(chuàng)建一個(gè)并存入緩存中,同時(shí)synchronized 包裹可以保證多線程同步,由此可見(jiàn)無(wú)論getSharedPreferences調(diào)用多少次,返回的都是一個(gè)SharedPreferencesImpl對(duì)象
SharedPreferencesImpl
SharedPreferencesImpl 實(shí)現(xiàn)了SharedPreferences這個(gè)接口,是我們通過(guò)getSharedPreferences得到的實(shí)體對(duì)象,所有存取操作都由該類(lèi)來(lái)實(shí)現(xiàn)
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file); //備份文件
mMode = mode;
mLoaded = false;
mMap = null;
startLoadFromDisk();
}
mBackupFile 代表發(fā)生異常時(shí), 可通過(guò)備份文件來(lái)恢復(fù)數(shù)據(jù).
mLoaded 表示是否已經(jīng)將mFile中的數(shù)據(jù)都讀取到mMap 中
mMap 用于在內(nèi)存中緩存我們的配置數(shù)據(jù), 也就是 getXxx 數(shù)據(jù)的來(lái)源
startLoadFromDisk()從方法名即可看出是從硬盤(pán)中讀取數(shù)據(jù),看一下源碼
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);
}
}
//...省略
Map map = null;
BufferedInputStream str = new BufferedInputStream(new FileInputStream(mFile), 16*1024);
map = XmlUtils.readMapXml(str);
//...省略
synchronized (mLock) {
mLoaded = true;
if (map != null) {
mMap = map;
} else {
mMap = new HashMap<>();
}
mLock.notifyAll();
}
}
開(kāi)啟一個(gè)子線程來(lái)從硬盤(pán)讀取數(shù)據(jù),如果備份文件存在則直接使用災(zāi)備文件回滾,使用XmlUtils把文件所有的數(shù)據(jù)讀取到內(nèi)存中的mMap中,mLoaded = true 標(biāo)志SharedPreferencesImpl已經(jīng)將數(shù)據(jù)讀取完成,notifyAll()喚醒getXXX系列方法等待狀態(tài)的線程,由于已經(jīng)將數(shù)據(jù)中磁盤(pán)讀取到內(nèi)存中,此時(shí)調(diào)用getXXX系列方法就可以獲取值了
getString分析
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
synchronized 關(guān)鍵字保證了線程安全,然后直接從mMap中獲取對(duì)應(yīng)的鍵值對(duì)就可以了,當(dāng)我們調(diào)用getSharedPreferences 之后馬上調(diào)用getString方法有可能SharedPreferencesImpl在子線程中還沒(méi)有將文件中的數(shù)據(jù)讀取完,此時(shí)mMap 還沒(méi)有被賦值,所以awaitLoadedLocked()將會(huì)阻塞當(dāng)前線程,直到讀取完畢
private void awaitLoadedLocked() {
while (!mLoaded) {
try {
mLock.wait();
} catch (InterruptedException unused) {
}
}
}
mLoaded為false表示尚未讀取完成,其他的getXXX系列方法和getString如出一轍,都是先等待文件讀取完畢,然后從mMap中獲取相應(yīng)的value
數(shù)據(jù)保存
我們通過(guò)getSharedPreferences().edit()來(lái)put各種值,看一下.edit()獲取的是一個(gè)什么對(duì)象
public Editor edit() {
synchronized (mLock) {
awaitLoadedLocked();
}
return new EditorImpl();
}
保證磁盤(pán)讀取完畢后,返回了一個(gè)新的EditorImpl對(duì)象
public final class EditorImpl implements Editor {
private final Object mLock = new Object();
private final Map<String, Object> mModified = Maps.newHashMap();
private boolean mClear = false;
public Editor putString(String key, @Nullable String value) {
synchronized (mLock) {
mModified.put(key, value);
return this;
}
}
public Editor putInt(String key, int value) {
synchronized (mLock) {
mModified.put(key, value);
return this;
}
}
public Editor remove(String key) {
synchronized (mLock) {
mModified.put(key, this);
return this;
}
}
...//省略
}
EditorImpl 中有兩個(gè)重要屬性,mModified 用來(lái)暫時(shí)保存put方法提供的值,當(dāng)調(diào)用commit()或者apply()才會(huì)將mModified中的數(shù)據(jù)存儲(chǔ)到mMap,進(jìn)而保存到磁盤(pán)中,mClear標(biāo)志是否要清空文件中所有數(shù)據(jù)。接下來(lái)需要注意看remove()方法,調(diào)用getSharedPreferences().edit().remove()時(shí)是將當(dāng)前key的value置為this,刪除數(shù)據(jù)時(shí)檢測(cè)到value為this即可刪除
總結(jié):調(diào)用put()后,數(shù)據(jù)只是暫存到了EditorImpl 的mModified** 對(duì)象中,并沒(méi)有回寫(xiě)到磁盤(pán),調(diào)用commit()或apply才會(huì)將數(shù)據(jù)寫(xiě)到磁盤(pán)中**
commit()
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;
}
return mcr.writeToDiskResult;
}
主要有三步
- commitToMemory將mModified 中的數(shù)據(jù)寫(xiě)到內(nèi)存mMap中
- SharedPreferencesImpl.this.enqueueDiskWrite 將內(nèi)存中mMap的數(shù)據(jù)回寫(xiě)到磁盤(pán)中
- mcr.writtenToDiskLatch.await() 線程等待,直到回寫(xiě)磁盤(pán)完畢
-
commitToMemory()
我們逐個(gè)分析,首先分析commitToMemory()返回一個(gè)MemoryCommitResult對(duì)象,代表了提交到內(nèi)存的返回結(jié)果
private static class MemoryCommitResult {
//...省略代碼
final Map<String, Object> mapToWriteToDisk;
//此處初始換CountDownLatch 的計(jì)數(shù)器為1
final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);
volatile boolean writeToDiskResult = false;
boolean wasWritten = false;
void setDiskWriteResult(boolean wasWritten, boolean result) {
this.wasWritten = wasWritten;
writeToDiskResult = result;
writtenToDiskLatch.countDown();
}
}
其中關(guān)鍵有 writtenToDiskLatch 是一個(gè) CountDownLatch 對(duì)象,它允許一個(gè)或多個(gè)線程一直等待,直到回寫(xiě)磁盤(pán)線程的操作執(zhí)行完后再執(zhí)行,mapToWriteToDisk引用內(nèi)存中的mMap,writeToDiskResult代表回寫(xiě)磁盤(pán)是否成功,接下來(lái)繼續(xù)分析commitToMemory()
private MemoryCommitResult commitToMemory() {
Map<String, Object> mapToWriteToDisk;
synchronized (SharedPreferencesImpl.this.mLock) {
mapToWriteToDisk = mMap;
//需要寫(xiě)入磁盤(pán)次數(shù)+1
mDiskWritesInFlight++;
synchronized (mLock) {
if (mClear) {
//...省略代碼,
//如果調(diào)用了edit().clear()則清空內(nèi)存中的數(shù)據(jù)
mMap.clear();
mClear = false;
}
//將putXXX()的數(shù)據(jù)提交到內(nèi)存中
for (Map.Entry<String, Object> e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
//value為this則刪除,與之前的getSharePreferences().edit().remove()對(duì)應(yīng)
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);
}
mDiskWritesInFlight代表寫(xiě)入磁盤(pán)這個(gè)操作的次數(shù),也是由synchronized 保證線程安全,首先判斷是否需要clear,如果需要這把mMap中的數(shù)據(jù)清空,需要注意此時(shí)mModified中數(shù)據(jù)還沒(méi)有復(fù)制到mMap中,所以以下代碼并不能將"foo" clear掉
sharedPreferences.edit()
.putBoolean("foo";, true) // foo 無(wú)法被 clear 掉
.clear()
.putBoolean("bar", true)
.commit()
然后通過(guò)for循環(huán)將put到mModified中的數(shù)據(jù)添加到mMap中,mModified.clear()之后返回MemoryCommitResult
總結(jié)commitToMemory()只是將數(shù)據(jù)都寫(xiě)入到內(nèi)存中
- SharedPreferencesImpl.this.enqueueDiskWrite
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
final boolean isFromSyncCommit = (postWriteRunnable == null);
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
//異步執(zhí)行任務(wù)
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
commit() 時(shí)postWriteRunnable參數(shù)為null,所以isFromSyncCommit == true,進(jìn)入到if (isFromSyncCommit) 語(yǔ)句中,如果此時(shí)只有一個(gè)commit()操作,則直接在當(dāng)前線程執(zhí)行writeToFile()將內(nèi)存中的數(shù)據(jù)回寫(xiě)到磁盤(pán)中,如果此時(shí)有多個(gè)commit()則,排隊(duì)進(jìn)入QueuedWork中等待執(zhí)行,看一下writeToFile()的實(shí)現(xiàn)
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
//...省略
boolean fileExists = mFile.exists();
boolean backupFileExists = mBackupFile.exists();
if (!backupFileExists) {
if (!mFile.renameTo(mBackupFile)) {
mcr.setDiskWriteResult(false, false);
return;
}
} else {
mFile.delete();
}
}
try {
FileOutputStream str = createFileOutputStream(mFile);
if (str == null) {
mcr.setDiskWriteResult(false, false);
return;
}
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
str.close();
mBackupFile.delete();
mcr.setDiskWriteResult(true, true);
return;
} catch (Exception e) {
}
//如果寫(xiě)入操作出現(xiàn)異常,則將半成品刪掉
if (mFile.exists()) {
if (!mFile.delete()) {
Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
}
}
mcr.setDiskWriteResult(false, false);
}
- 將之前的配置文件mFile備份為buckup文件,然后刪除
- mcr.mapToWriteToDisk即內(nèi)存中數(shù)據(jù),全部寫(xiě)入到新的mFile中
- 寫(xiě)入成功,刪掉備份文件,如果寫(xiě)入失敗則把半成品mFile刪掉
-
mcr.writtenToDiskLatch.await()
CountDownLatch.await()會(huì)阻塞當(dāng)前線程,直到CountDownLatch.countDown()使計(jì)數(shù)器值到達(dá)0時(shí),它表示磁盤(pán)寫(xiě)入線程已經(jīng)完成了任務(wù),然后在鎖上等待的線程就可以恢復(fù)執(zhí)行任務(wù)。在writeToFile()中,寫(xiě)入完成之后會(huì)調(diào)用mcr.setDiskWriteResult()中的writtenToDiskLatch.countDown()
void setDiskWriteResult(boolean wasWritten, boolean result) {
this.wasWritten = wasWritten;
writeToDiskResult = result;
writtenToDiskLatch.countDown();
}
writtenToDiskLatch初始時(shí)計(jì)數(shù)器為1,countDown()之后為0,此時(shí)磁盤(pán)已經(jīng)回寫(xiě)完畢,commit()方法繼續(xù)執(zhí)行,返回結(jié)果
commit()總結(jié):
- 流程是先寫(xiě)入內(nèi)存在寫(xiě)入磁盤(pán)
- 寫(xiě)入磁盤(pán)完成之前調(diào)用線程會(huì)一直等待,直到內(nèi)存和磁盤(pán)都已經(jīng)同步完畢
- 每次寫(xiě)入磁盤(pán)時(shí)都會(huì)從內(nèi)存中將所有數(shù)據(jù)都全量寫(xiě)入,效率并不高
apply()
public void apply() {
//第一步:提交到內(nèi)存
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
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");
}
}
};
//第二步:確保異步磁盤(pán)寫(xiě)入完畢
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
// 第三步:寫(xiě)入磁盤(pán)
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
}
- 第一步提交到內(nèi)存和commit()是一樣的
- 第二步中將任務(wù)mcr.writtenToDiskLatch.await()提交到QueuedWork之中,該任務(wù)的作用是讓線程等待,而釋放的時(shí)機(jī)跟commit()一樣(詳細(xì)代碼看上述commit()),但是QueuedWork.addFinisher()將線程等待的任務(wù)提交之后并沒(méi)有立即運(yùn)行,而是保存在了一個(gè)隊(duì)列之中,當(dāng)應(yīng)用收到系統(tǒng)廣播,或者被調(diào)用 onPause 等一些時(shí)機(jī)才會(huì)運(yùn)行(詳情查看QueuedWork源碼,在ActivityThread中可以找到調(diào)用任務(wù)的方法waitToFinish())
- 第三步同commit,不同點(diǎn)在于enqueueDiskWrite(mcr, postWriteRunnable)傳遞了Runnable,在異步線程中寫(xiě)入磁盤(pán)
apply總結(jié) - 異步寫(xiě)入磁盤(pán),沒(méi)有等待結(jié)果,直接返回
- 應(yīng)用收到系統(tǒng)廣播,或者被調(diào)用 onPause等時(shí)機(jī),如果磁盤(pán)寫(xiě)入未完成則主線程會(huì)等待其完成
- 和commit()寫(xiě)入過(guò)程一樣,都是全量寫(xiě)入
SharedPreferences總結(jié)
通過(guò)上文對(duì)SharedPreferences分析,我們已經(jīng)可以對(duì)開(kāi)頭的幾個(gè)問(wèn)題進(jìn)行回答并總結(jié)了
- 數(shù)據(jù)是如何保存到磁盤(pán)的
答:通過(guò)putXXX系列方法將數(shù)據(jù)先保存到內(nèi)存中,調(diào)用commit()或者apply(之后將所有數(shù)據(jù)全量寫(xiě)入磁盤(pán)文件中 - commit() 和apply()的區(qū)別
答:commit()線程同步寫(xiě)入,寫(xiě)入完成時(shí)才會(huì)返回,如果在主線程調(diào)用,寫(xiě)入過(guò)程比較費(fèi)時(shí)可能會(huì)阻塞主線程,
apply異步線程寫(xiě)入,但是應(yīng)用收到系統(tǒng)廣播,或者被調(diào)用 onPause等時(shí)機(jī),未完成寫(xiě)入任務(wù)時(shí)主線程會(huì)等待其完成 - commit()和apply()相同點(diǎn)
答:都是全量寫(xiě)入,如果SharedPreferences中數(shù)據(jù)量很多,則每次寫(xiě)入都會(huì)很慢 - 為什么會(huì)造成ANR
答:commit()和apply()都可能在成ANR,分析如上 - SharedPreferences有哪些缺點(diǎn)
答:1. 全量寫(xiě)入:commit() 還是 apply(),即使我們只改動(dòng)其中一條數(shù)據(jù),都會(huì)把整個(gè)數(shù)據(jù)寫(xiě)入到文件中
2. 卡頓:commit() 還是 apply()都有可能造成ANR
3. 跨進(jìn)程不安全:MODE_MULTI_PROCESS已被谷歌標(biāo)為Deprecated
總之:系統(tǒng)提供的 SharedPreferences 的應(yīng)用場(chǎng)景是用來(lái)存儲(chǔ)一些簡(jiǎn)單、輕量的數(shù)據(jù),例如配置文件等,不適合json、html等,并且每個(gè)SharedPreference不宜過(guò)大,考慮將頻繁修改的配置項(xiàng)單獨(dú)隔離