前言
轉(zhuǎn)載
我們知道 SharedPreferences 會從文件讀取 xml 文件, 并將其以 getXxx/putXxx 的形式提供讀寫服務. 其中涉及到如下幾個問題:
- 如何從磁盤讀取配置到內(nèi)存
- getXxx 如何從內(nèi)存中獲取配置
- 最終配置如何從內(nèi)存回寫到磁盤
- 多線程/多進程是否會有問題
- 最佳實踐
1. 結論
- SharedPreferences 是線程安全的峡钓,內(nèi)部由大量 synchronized 關鍵字保障。
- SharedPreferences 不是進程安全的盐固。
- 第一次 getSharedPreferences 會讀取磁盤文件,后續(xù)的 getSharedPreferences 會從內(nèi)存緩存中獲取实幕。 如果第一次調(diào)用 getSharedPreferences 時還沒從磁盤加載完畢就調(diào)用 getXxx/putXxx 无埃, 則 getXxx/putXxx 操作會卡主,直到數(shù)據(jù)從磁盤加載完畢后返回山宾。
- 所有的 getXxx 都是從內(nèi)存中取的數(shù)據(jù)
- apply 是同步回寫內(nèi)存俏让,然后把異步回寫磁盤的任務放到一個單線程的隊列中等待調(diào)度楞遏。commit 和前者一樣,只不過要等待異步磁盤任務結束后才返回首昔。
- MODE_MULTI_PROCESS 是在每次 getSharedPreferences 時檢查磁盤上配置文件上次修改時間和文件大小寡喝, 一旦所有修改則會重新從磁盤加載文件。 所以并不能保證多進程數(shù)據(jù)的實時同步勒奇。
- 從 Android N 開始预鬓,不再支持 MODE_WORLD_READABLE & MODE_WORLD_WRITEABLE。一旦指定赊颠,會拋異常格二。
2. 最佳實踐
- 不要多進程使用,很小幾率會造成數(shù)據(jù)全部丟失(萬分之一)竣蹦, 現(xiàn)象是配置文件被刪除顶猜。
- 不要依賴 MODE_MULTI_PROCESS,這個標記就像 MODE_WORLD_READABLE/MODE_WORLD_WRITEABLE 未來會被廢棄痘括。
- 每次 apply / commit 都會把全部的數(shù)據(jù)一次性寫入磁盤密任,所以單個的配置文件不應該過大涂炎, 影響整體性能碳默。
3. 源碼分析
3.1 SharedPreferences 對象的獲取
一般來說有如下方式:
- PreferenceManager.getDefaultSharedPreferences
- ContextImpl.getSharedPreferences
我們以上述 [1] 為例來看看源碼:
// PreferenceManager.java
public static SharedPreferences getDefaultSharedPreferences(Context context) {
return context.getSharedPreferences(getDefaultSharedPreferencesName(context),
getDefaultSharedPreferencesMode());
}
可以看到最終也是調(diào)用到了 ContextImpl.getSharedPreferences, 源碼:
// ContextImpl.java
@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) {
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");
}
}
sp = new SharedPreferencesImpl(file, mode);
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;
}
可見 sdk 是先取了緩存猛遍,如果緩存未命中,才構造對象驰后。也就是說,多次 getSharedPreferences 幾乎是沒有代價的矗愧。 同時灶芝, 實例的構造被 synchronized
關鍵字包裹郑原,因此構造過程是多線程安全的。
3.2 SharedPreferences 的構造
第一次構建對象時:
// SharedPreferencesImpl.java
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
startLoadFromDisk();
}
有這么幾個關鍵信息:
-
mFile
代表我們磁盤上的配置文件夜涕。 -
mBackupFile
是一個災備文件犯犁,用戶寫入失敗時進行恢復,后面會再說女器。其路徑是 mFile 加后綴 ‘.bak’酸役。 -
mMap
用于在內(nèi)存中緩存我們的配置數(shù)據(jù), 也就是 getXxx 數(shù)據(jù)的來源。
還涉及到一個 startLoadFromDisk驾胆, 我們來看看:
// SharedPreferencesImpl.java
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
開啟了一個線程從文件讀取涣澡, 其源碼如下:
// SharedPreferencesImpl.java
private void loadFromDisk() {
synchronized (SharedPreferencesImpl.this) {
if (mLoaded) {
return;
}
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
}
... 略去無關代碼 ...
str = new BufferedInputStream(
new FileInputStream(mFile), 16*1024);
map = XmlUtils.readMapXml(str);
synchronized (SharedPreferencesImpl.this) {
mLoaded = true;
if (map != null) {
mMap = map;
mStatTimestamp = stat.st_mtime;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
notifyAll();
}
}
loadFromDisk
這個函數(shù)很關鍵。它就是實際從磁盤讀取配置文件的函數(shù)丧诺。 可見入桂, 它做了如下幾件事:
- 如果有 ‘災備’ 文件,則直接使用災備文件回滾驳阎。
- 把配置從磁盤讀取到內(nèi)存的并保存在 mMap 字段中(看代碼最后 mMap = map)抗愁。
- 標記讀取完成, 這個字段后面
awaitLoadedLocked
會用到呵晚。 記錄讀取文件的時間蜘腌,后面MODE_MULTI_PROCESS
中會用到。 - 發(fā)一個
notifyAll
通知已經(jīng)讀取完畢饵隙, 激活所有等待加載的其他線程撮珠。
總結一下:
3.3 getXxx 的流程
// SharedPreferencesImpl.java
public String getString(String key, @Nullable String defValue) {
synchronized (this) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
可見, 所有的 get 操作都是線程安全的癞季。并且 get 僅僅是從內(nèi)存中(mMap) 獲取數(shù)據(jù)劫瞳, 所以無性能問題。
考慮到配置文件的加載是在單獨的線程中異步進行的(參考 ‘SharedPreferences 的構造’)绷柒,所以這里的 awaitLoadedLocked
是在等待配置文件加載完畢志于。 也就是說如果我們第一次構造 SharedPreferences 后就立刻調(diào)用 getXxx 方法, 很有可能讀取配置文件的線程還未完成废睦, 所以這里要等待該線程做完相應的加載工作伺绽。
來看看 awaitLoadedLocked 的源碼:
// SharedPreferencesImpl.java
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);
}
}
很明顯,如果加載還未完成(mLoaded == false)嗜湃,getXxx 會卡在 awaitLoadedLocked奈应,一旦加載配置文件的線程工作完畢, 則這個加載線程會通過 notifyAll
會通知所有在 awaitLoadedLocked
中等待的線程购披,getXxx 就能夠返回了杖挣。不過大部分情況下,mLoaded == true
刚陡。這樣的話 awaitLoadedLocked
會直接返回惩妇。
3.4 putXxx 的流程
set 比 get 稍微麻煩一點兒株汉,因為涉及到 Editor
和 MemoryCommitResult
對象。
先來看看 edit() 方法的實現(xiàn):
// SharedPreferencesImpl.java
public Editor edit() {
// TODO: remove the need to call awaitLoadedLocked() when
// requesting an editor. will require some work on the
// Editor, but then we should be able to do:
//
// context.getSharedPreferences(..).edit().putString(..).apply()
//
// ... all without blocking.
synchronized (this) {
awaitLoadedLocked();
}
return new EditorImpl();
}
Editor
Editor 沒有構造函數(shù)歌殃,只有兩個屬性被初始化:
// SharedPreferencesImpl.java
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;
... 略去方法定義 ...
public Editor putString(String key, @Nullable String value) { ... }
public boolean commit() { ... }
...
}
mModified
是我們每次 putXxx 后所改變的配置項乔妈。
mClear
標識要清空配置項, 但是只清了 SharedPreferences.mMap
氓皱。
edit() 會保障配置已從磁盤讀取完畢路召,然后僅僅創(chuàng)建了一個對象。接下來看看 putXxx 的真身:
// SharedPreferencesImpl.java
public Editor putString(String key, @Nullable String value) {
synchronized (this) {
mModified.put(key, value);
return this;
}
}
很簡單波材,僅僅是把我們設置的配置項放到了 mModified
屬性里保存股淡。等到 apply
或者 commit
的時候回寫到內(nèi)存和磁盤。咱們分別來看看各聘。
apply
apply
是各種 ‘最佳實踐’ 推薦的方式揣非,那么它到底是怎么異步工作的呢?我們來看個究竟:
// SharedPreferencesImpl.java
public void apply() {
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
public void run() {
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
}
};
QueuedWork.add(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
public void run() {
awaitCommit.run();
QueuedWork.remove(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);
}
可以看出大致的脈絡:
-
commitToMemory
應該是把修改的配置項回寫到內(nèi)存躲因。 -
QueuedWork.add(awaitCommit)
貌似沒什么卵用早敬。 -
SharedPreferencesImpl.this.enqueueDiskWrite
把配置項加入到一個異步隊列中,等待調(diào)度大脉。
我們來看看 commitToMemory
的實現(xiàn)(略去大量無關代碼):
// SharedPreferencesImpl.java
private MemoryCommitResult commitToMemory() {
MemoryCommitResult mcr = new MemoryCommitResult();
synchronized (SharedPreferencesImpl.this) {
... 略去無關 ...
mapToWriteToDisk = mMap;
mDiskWritesInFlight++;
boolean hasListeners = mListeners.size() > 0;
if (hasListeners) {
keysModified = new ArrayList<String>();
listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
}
synchronized (mEditorLock) {
boolean changesMade = false;
if (mClear) {
if (!mapToWriteToDisk.isEmpty()) {
changesMade = true;
mapToWriteToDisk.clear();
}
mClear = false;
}
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 (!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);
}
}
總結來說就兩件事:
- 把
Editor.mModified
中的配置項回寫到 SharedPreferences.mMap 中搞监,完成了內(nèi)存的同步。 - 把 SharedPreferences.mMap 保存在了
mcr.mapToWriteToDisk
中镰矿。而后者就是即將要回寫到磁盤的數(shù)據(jù)源琐驴。
我們再來回頭看看 apply 方法:
// SharedPreferencesImpl.java
public void apply() {
final MemoryCommitResult mcr = commitToMemory();
... 略無關 ...
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
}
-
commitToMemory
完成了內(nèi)存的同步回寫 -
enqueueDiskWrite
完成了硬盤的異步回寫, 我們接下來具體看看 enqueueDiskWrite
// SharedPreferencesImpl.java
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr);
}
...
}
};
...
QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}
QueuedWork.singleThreadExecutor 實際上就是 ‘一個線程的線程池’, 如下:
// QueuedWork.java
public static ExecutorService singleThreadExecutor() {
synchronized (QueuedWork.class) {
if (sSingleThreadExecutor == null) {
// TODO: can we give this single thread a thread name?
sSingleThreadExecutor = Executors.newSingleThreadExecutor();
}
return sSingleThreadExecutor;
}
}
回到 enqueueDiskWrite
中秤标,這里還有一個重要的函數(shù)叫做 writeToFile
:
writeToFile
// SharedPreferencesImpl.java
private void writeToFile(MemoryCommitResult mcr) {
// Rename the current file so it may be used as a backup during the next read
if (mFile.exists()) {
if (!mBackupFile.exists()) {
if (!mFile.renameTo(mBackupFile)) {
return;
}
} else {
mFile.delete();
}
}
// Attempt to write the file, delete the backup and return true as atomically as
// possible. If any exception occurs, delete the new file; next time we will restore
// from the backup.
try {
FileOutputStream str = createFileOutputStream(mFile);
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
try {
final StructStat stat = Os.stat(mFile.getPath());
mStatTimestamp = stat.st_mtime;
mStatSize = stat.st_size;
}
// Writing was successful, delete the backup file if there is one.
mBackupFile.delete();
return;
}
// Clean up an unsuccessfully written file
mFile.delete();
}
代碼大致分為三個過程:
- 先把已存在的老的配置文件重命名(加 ‘.bak’ 后綴)绝淡, 然后刪除老的配置文件。這相當于做了災備苍姜。
- 向
mFile
中一次性寫入所有配置項牢酵。即mcr.mapToWriteToDisk
(這就是 commitToMemory 所說的保存了所有配置項的字段) 一次性寫入到磁盤。如果寫入成功則刪除災備文件衙猪,同時記錄了這次同步的時間馍乙。 - 如果上述過程 [2] 失敗,則刪除這個半成品的配置文件垫释。
好了, 我們來總結一下 apply:
- 通過 commitToMemory 將修改的配置項同步回寫到內(nèi)存 SharedPreferences.mMap 中丝格。此時,任何的 getXxx 都可以獲取到最新數(shù)據(jù)了棵譬。
- 通過
enqueueDiskWrite
調(diào)用 writeToFile 將所有配置項一次性異步回寫到磁盤显蝌, 這是一個單線程的線程池。
時序圖:
commit
看過了 apply
再看 commit
就非常容易了订咸。
// SharedPreferencesImpl.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;
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
時序圖:
只需關注最后一條 ‘等待異步任務返回’ 的線曼尊,對比 apply
的時序圖, 一眼就看出差別扭屁。
registerOnSharedPreferenceChangeListener
最后需要提一下的就是 listener:
- 對于 apply,listener 回調(diào)時內(nèi)存已經(jīng)完成同步, 但是異步磁盤任務不保證是否完成涩禀。
- 對于 commit, listener 回調(diào)時內(nèi)存和磁盤都已經(jīng)同步完畢然眼。
申明:開始的圖片來源網(wǎng)絡艾船,侵刪