先來一波靈魂追問:java
( 年底福利: 知道你很忙,參考答案可直接看文末... )git
一切從getSharedPreference(String name,int Mode)這個方法提及;經過這個方法獲取到一個SharedPreference實例。SharedPreferences是一個接口(interface),他的具體實現類爲SharedPreferencesImpl。 SharedPreference的加載的主要過程:github
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
private ArrayMap<String, File> mSharedPrefsPaths;
複製代碼
sSharedPrefsCache存儲的是File和SharedPreferencesImpl鍵值對,當對應File的SharedPreferencesImpl加載以後就會一支存儲於sSharedPrefsCache中。相似的mSharedPrefsPaths存儲的是name和File的對應關係。使用的ArrayMap,關於ArrayMap這種Android特有的數據結構,詳細瞭解能夠看這juejin.im/post/5d550f…微信
當經過name最終找到對應的File以後,就會實例化一個SharedPreferencesImpl對象。在SharedPreferences構造方法中開啓一個子線程加載磁盤中的xml文件。數據結構
你們都應該很明確的一點是,SP持久化的本質是在本地磁盤記錄了一個xml文件,這個文件所在的文件夾shared_prefsapp
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
複製代碼
怎麼保證使用sp.get(String name)的時候SP的初始化或者說從磁盤中加載到內存中這一過程已經完成了呢?異步
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() {
if (!mLoaded) {
// Raise an explicit StrictMode onReadFromDisk for this
// thread, since the real read will be in a different
// thread and otherwise ignored by StrictMode.
BlockGuard.getThreadPolicy().onReadFromDisk();
}
while (!mLoaded) {
try {
mLock.wait();
} catch (InterruptedException unused) {
}
}
if (mThrowable != null) {
throw new IllegalStateException(mThrowable);
}
}
複製代碼
使用awaitLoadedLocked()方法檢測,是否已經加載完成,若是沒有加載完成,就等待堵塞。等加載完成以後,繼續執行;ide
在loadFromDisk()方法中,若是加載成功會把mLoaded標誌位置爲true,而後 mLock.notifyAll();post
最終,就把位於磁盤中的文件,加載到了內存中對應一個SharedPreferces對象,SharedPreferences中mMap。ui
當想SP中存入數據的時候,實例代碼以下。
sharedPreferences.edit().putInt("number", 100).puString("age","18").apply();
sharedPreferences.edit().putInt("number", 100).commit();
複製代碼
調用sharedPreferences.edit()返回一個EditorImpl對象,操做數據以後調用apply()或者commit()。
@Override
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 {}
notifyListeners(mcr);//通知監聽
return mcr.writeToDiskResult;
}
//
private void enqueueDiskWrite(final MemoryCommitResult mcr,final Runnable postWriteRunnable) {
//若是postWriteRunnable爲空表示來自commit()方法調用
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提交,且mDiskWritesInFlight爲1的時候,直接在當前所在線程執行寫入磁盤操做
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
//交個QueuedWork,QueuedWork內部維護了一個HandlerThread,一直執行寫入磁盤操做。
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
複製代碼
如註釋:當調用commit()方法以後
首先將編輯的結果同步到內存中。
enqueueDiskWrite()將這個結果同步到磁盤中,enqueueDiskWrite()的第二個參數postWriteRunnable傳入空。一般狀況下也就是mDiskWritesInFlight(正在執行的寫入磁盤操做的數量)爲1的時候,直接在當前所在線程執行寫入磁盤操做。不然仍是異步到QueuedWork中去執行。commit()時,寫入磁盤操做會發生在當前線程的說法是不許確的。
執行mcr.writtenToDiskLatch.await(); MemoryCommitResult 中有個一個CountDownLatch 成員變量,他的具體做用能夠查閱其餘資料。總的來講,當前線程執行會堵塞在這,直到mcr.writtenToDiskLatch知足了條件。也就是當寫入磁盤成功以後,會繼續執行下面的操做。
因此,commit提交以後會有返回結果,同步堵塞直到有返回結果。
@Override
public void apply() {
final long startTime = System.currentTimeMillis();
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {mcr.writtenToDiskLatch.await();}
};
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
notifyListeners(mcr);
}
複製代碼
/** Delay for delayed runnables, as big as possible but low enough to be barely perceivable */
private static final long DELAY = 100;
public static void queue(Runnable work, boolean shouldDelay) {
Handler handler = getHandler();
synchronized (sLock) {
sWork.add(work);
if (shouldDelay && sCanDelay) {
handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
} else {
handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
}
}
複製代碼
You don't need to worry about Android component lifecycles and their interaction with apply() writing to disk. The framework makes sure in-flight disk writes from apply() complete before switching states.
官方文檔中有這樣段化話,意思是您不須要擔憂Android組件生命週期及其對apply()寫入磁盤的影響。框層架確保在切換狀態以前完成使用apply()方法正在執行磁盤寫入的動做。
然而還真是不讓人那麼省心。
罪魁禍首在這:
//QueuedWork.java
public static void waitToFinish() {
...
processPendingWork();//執行文件寫入磁盤操做
....
}
private static void processPendingWork() {
long startTime = 0;
....
if (work.size() > 0) {
for (Runnable w : work) {
w.run();
}
...
}
複製代碼
waitToFinish()會將,儲存在QueuedWork的操做一併處理掉。何時呢?在Activiy的 onPause()、BroadcastReceiver的onReceive()以及Service的onStartCommand()方法以前都會調用waitToFinish()。你們知道這些方法都是執行在主線程中,一旦waitToFinish()執行超時,就會跑出ANR。
至於waitToFinish調用具體時機,查看ActivityThread.java類文件。這裏只是說本質原理。
\\ContextImpl private void checkMode(int mode) {
if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.N) {
if ((mode & MODE_WORLD_READABLE) != 0) {
throw new SecurityException("MODE_WORLD_READABLE no longer supported");
}
if ((mode & MODE_WORLD_WRITEABLE) != 0) {
throw new SecurityException("MODE_WORLD_WRITEABLE no longer supported");
}
}
}
複製代碼
Andorid 7.0及以上會拋出異常,Sharepreferences再也不支持多進程模式。多進程共享文件會出現問題的本質在於,由於不一樣進程,因此線程同步會失效。要解決這個問題,可嘗試跨進程解決方案,如ContentProvider、AIDL、AIDL、Service。
既然SharedPreferences有這麼多問題?就沒人管管嗎? 溫和的治理方法或者說小建議
經過本文咱們瞭解了SharedPreferences的基本原理。再回頭看看文章開頭的那幾個問題,是否是有答案了。