先給出結(jié)論,如果不想跟隨源碼分析的,可以根據(jù)結(jié)論,對SharedPreferences有個大概的了解。
結(jié)論:
1.SharedPreferences 線程是安全的,內(nèi)部由大量synchronized代碼塊實現(xiàn)同步
2.SharedPreferences 進程是不安全的(雖然官方提供了 ),但官方文檔也給出了如下說明:MODE_MULTI_PROCESS加載模式,該加載模式在API級別11中添加,在API級別23中被廢棄。該SharedPreference loading標志:設置后,即使已在此過程中加載了共享首選項實例,也會檢查磁盤上的文件是否已修改。在應用程序具有多個進程的情況下,有時需要此行為,所有進程都寫入相同的SharedPreferences文件。但是,通常在進程之間存在更好的通信形式。
此常量在API級別23中已棄用
.MODE_MULTI_PROCESS在某些Android版本中無法可靠地工作,并且不提供任何協(xié)調(diào)跨進程的并發(fā)修改的機制。應用程序不應嘗試使用它。相反,他們應該使用明確的跨流程數(shù)據(jù)管理方法,例如ContentProvider。
3.首次getSharedPreferences會把文件從磁盤加載到內(nèi)存(用Map進行保存),之后調(diào)用getSharedPreferences獲取數(shù)據(jù),即從內(nèi)存中直接獲取,也就不會由于大量的synchronized而影響性能
4.apply:同步修改內(nèi)存中的數(shù)據(jù),然后異步回寫到磁盤且是將其放入單線程任務隊列中執(zhí)行
commit:同步修改內(nèi)存中的數(shù)據(jù),且同步回寫到磁盤,因此會阻塞當前線程
建議:
1.不要使用多進程操作SharedPreferences ,小概率會出現(xiàn)數(shù)據(jù)丟失(文件被刪除)
2.不要存儲大量的數(shù)據(jù),從磁盤加載SharedPreferences 文件是進行I/O操作的,并且由于文件結(jié)構(gòu)是xml格式的,加載解析都比較耗時,雖然在此過程中是異步的,但如果數(shù)據(jù)過大,首次調(diào)用getSharedPreferences后,然后馬上執(zhí)行getXX()或者putXX()由于需要等待加載文件結(jié)束,可能會阻塞線程(如果在UI線程可能會引發(fā)ANR)。并且SharedPreferences 中的數(shù)據(jù)會一次性從磁盤加載到內(nèi)存,.apply、commit都是會將數(shù)據(jù)寫到磁盤的,SharedPreferences 文件不應過大會影響性能
3.盡量使用apply更新數(shù)據(jù),因為它是異步寫入磁盤的,不會阻塞線程
將您的首選項更改從此編輯器提交回它正在編輯的SharedPreferences對象。這將自動執(zhí)行請求的修改,替換SharedPreferences中當前的內(nèi)容。
注意,當兩個編輯器同時修改首選項時,最后一個調(diào)用apply的編輯器將獲勝。
與同步地將其首選項寫到持久存儲的commit()不同,apply()將其更改立即提交到內(nèi)存中的SharedPreferences,但是啟動到磁盤的異步提交,并且不會通知您任何失敗。如果這個SharedPreferences上的另一個編輯器在apply()仍然未完成時執(zhí)行常規(guī)提交(),那么commit()將阻塞,直到所有異步提交和提交本身都完成為止。
由于SharedPreferences實例是流程中的單例實例,如果已經(jīng)忽略了返回值,那么可以用apply()替換commit()的任何實例。
您不需要擔心Android組件的生命周期以及它們與向磁盤寫入apply()的交互。該框架確保在切換狀態(tài)之前完成來自apply()的在途磁盤寫操作。
源碼分析:
我們通過調(diào)用getSharedPreferences獲取SharedPreferences對象時,最終會調(diào)用ContextImpl.getSharedPreferences(File file, int mode)方法:
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
//獲取緩存中所有SharedPreferences對象集合
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
//緩存中是否已存在我們需要的SharedPreferences
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");
}
}
//如果緩存中不存在,則通過創(chuàng)建SharedPreferencesImpl對象,由其加載文件
sp = new SharedPreferencesImpl(file, mode);
//將SharedPreferences對象存入緩存集合中,以便下次直接從內(nèi)存中獲取
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;
}
先從緩存中獲取,如果未命中則創(chuàng)建SharedPreferencesImpl對象,并保存到緩存中,多次調(diào)用getSharedPreferences并不會多次創(chuàng)建對象,由于整個SharedPreferences獲取過程中都放在synchronized代碼塊中,因此在多線程下創(chuàng)建對象是安全的
SharedPreferencesImpl創(chuàng)建過程:
SharedPreferencesImpl(File file, int mode) {
//需要加載的SharedPreferences文件對象
mFile = file;
//備份文件對象,寫入失敗時會通過其進行數(shù)據(jù)恢復
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
//數(shù)據(jù)緩存集合,用于存儲SharedPreferences文件中的數(shù)據(jù)
mMap = null;
mThrowable = null;
//從磁盤加載數(shù)據(jù)
startLoadFromDisk();
}
下面我們看看從磁盤加載數(shù)據(jù)過程:
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
//從磁盤加載數(shù)據(jù)
loadFromDisk();
}
}.start();
}
另起一個線程加載SharedPreferences文件
private void loadFromDisk() {
synchronized (mLock) {
if (mLoaded) {
return;
}
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
}
···省略代碼···
str = new BufferedInputStream(new FileInputStream(mFile), 16 * 1024);
map = (Map<String, Object>) XmlUtils.readMapXml(str);
···省略代碼···
synchronized (mLock) {
mLoaded = true;
mThrowable = thrown;
···省略代碼···
if (map != null) {
mMap = map;
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
...
mLock.notifyAll();
}
}
從上面方法可知從磁盤加載SharedPreferences文件分別做了一下幾步處理:
1.如果由備份文件,把原文件刪除,然后修改備份文件名稱,使其成為原文件,達到文件恢復效果(備份文件名以.bak結(jié)尾)
2.加載文件并解析成Map
3.標記加載完成
4.將Map賦值給mMap(即數(shù)據(jù)緩存集合,用于存儲SharedPreferences文件中的數(shù)據(jù))
5.通過notifyAll喚起所有等待加載的線程
現(xiàn)在SharedPreferences對象已經(jīng)初始化成功了,下面我們看一下對其數(shù)據(jù)操作的流程:
getXX()
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
//等待SharedPreferences加載完成
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
通過synchronized代碼塊,可見獲取數(shù)據(jù)是線程安全的,僅僅是從緩存中獲取數(shù)據(jù),同步不會影響性能。而awaitLoadedLocked()即是等待SharedPreferences文件從磁盤加載到內(nèi)存完畢后再執(zhí)行下面的獲取數(shù)據(jù)的方法,因此如果在首次調(diào)用getSharedPreferences時,馬上調(diào)用getXX(),會阻塞線程
接著看看awaitLoadedLocked()里面做了如何處理:
private void awaitLoadedLocked() {
if (!mLoaded) {
BlockGuard.getThreadPolicy().onReadFromDisk();
}
while (!mLoaded) {
try {
mLock.wait();
} catch (InterruptedException unused) {
}
}
if (mThrowable != null) {
throw new IllegalStateException(mThrowable);
}
}
可見,除了首次加載文件時,mLoaded = false會卡在此處等待,當文件加載完畢后會重置加載狀態(tài)即mLoaded = true,則不會等待直接獲取數(shù)據(jù)
putXX()
修改數(shù)據(jù)時首先得獲取Editor對象(SharedPreferencesImpl.java)
@Override
public Editor edit() {
synchronized (mLock) {
awaitLoadedLocked();
}
return new EditorImpl();
}
public final class EditorImpl implements Editor {
private final Object mEditorLock = new Object();
@GuardedBy("mEditorLock")
private final Map<String, Object> mModified = new HashMap<>();
@GuardedBy("mEditorLock")
private boolean mClear = false;
@Override
public Editor putString(String key, @Nullable String value) {
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
}
···代碼省略···
}
可見我們調(diào)用putXX()僅僅是將數(shù)據(jù)存到mModified中,直到我們調(diào)用apply或commit時才真正將數(shù)據(jù)同步到緩存并寫入磁盤
apply()
@Override
public void apply() {
final long startTime = System.currentTimeMillis();
//同步數(shù)據(jù)到緩存
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
}
};
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);
}
下面看看同步緩存數(shù)據(jù)流程:
private MemoryCommitResult commitToMemory() {
···代碼省略···
synchronized (SharedPreferencesImpl.this.mLock) {
···代碼省略···
//緩存數(shù)據(jù)集合
mapToWriteToDisk = mMap;
···代碼省略···
synchronized (mEditorLock) {
boolean changesMade = false;
if (mClear) {
if (!mapToWriteToDisk.isEmpty()) {
changesMade = true;
mapToWriteToDisk.clear();
}
mClear = false;
}
//同步緩存數(shù)據(jù)
for (Map.Entry<String, Object> e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
if (v == this || v == null) {
if (!mapToWriteToDisk.containsKey(k)) {
continue;
}
mapToWriteToDisk.remove(k);
} else {
if (mapToWriteToDisk.containsKey(k)) {
Object existingValue = mapToWriteToDisk.get(k);
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
mapToWriteToDisk.put(k, v);
}
changesMade = true;
if (hasListeners) {
keysModified.add(k);
}
}
mModified.clear();
if (changesMade) {
mCurrentMemoryStateGeneration++;
}
memoryStateGeneration = mCurrentMemoryStateGeneration;
}
}
return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,mapToWriteToDisk);
}
然后我們瞅瞅?qū)懭氪疟PSharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) {
//如果是commit則postWriteRunnable == null即isFromSyncCommit = true
//如果是apply則isFromSyncCommit = false
final boolean isFromSyncCommit = (postWriteRunnable == null);
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
//寫入磁盤
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
// commit直接在當前線程直接同步寫入磁盤
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
//apply加入任務隊列執(zhí)行寫入磁盤runnable
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
public static void queue(Runnable work, boolean shouldDelay) {
Handler handler = getHandler();
synchronized (sLock) {
sWork.add(work);
//sCanDelay用于紀錄當前線程是否處于寫入磁盤任務狀態(tài)
if (shouldDelay && sCanDelay) {
handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
} else {
handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
}
}
}
private static Handler getHandler() {
synchronized (sLock) {
if (sHandler == null) {
//首次創(chuàng)建一個線程
HandlerThread handlerThread = new HandlerThread("queued-work-looper",
Process.THREAD_PRIORITY_FOREGROUND);
handlerThread.start();
//創(chuàng)建一個handler用于接收queue(Runnable work, boolean shouldDelay)發(fā)送的消息,然后執(zhí)行寫入磁盤runnable
sHandler = new QueuedWorkHandler(handlerThread.getLooper());
}
return sHandler;
}
}
commit()
@Override
public boolean commit() {
long startTime = 0;
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");
}
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
由此可見,不論commit/apply都回調(diào)用SharedPreferencesImpl.this.enqueueDiskWrite然后同步/異步執(zhí)行寫入磁盤操作