SharedPreferences作為一種數(shù)據(jù)持久化的方式,是處理簡單的key-value類型數(shù)據(jù)時的首選。但是有時候需要在多進程中共享數(shù)據(jù)時,如果用SharedPreferences會不會有什么問題?即SharedPreferences是否是進程安全的?
SharedPreferences如何實現(xiàn)多進程訪問?
談論進程安全前,先看下SharedPreferences如何實現(xiàn)多進程。我們知道每個SharedPreferences都對應了當前package的data/data/package_name/share_prefs/目錄下的一個文件,要讓多個進程可訪問這個文件,必然需要修改其訪問權(quán)限,看下SharedPreferences提供了哪些選項, 在Context.java中提供了以下幾個mode:
int MODE_PRIVATE = 0x0000;
這是默認模式,僅caller uid的進程可訪問
int MODE_WORLD_READABLE = 0x0001;
所有人可寫,也就是任何應用都可修改它,這是極其危險的,因此改選項已被Deprected
int MODE_WORLD_READABLE = 0x0002
所有人可讀,這個參數(shù)同樣非常危險,可能導致隱私數(shù)據(jù)泄漏
int MODE_MULTI_PROCESS = 0x0004;
設置該參數(shù)后,每次獲取對應的SharedPreferences時都會嘗試從磁盤中讀取修改過的文件
多進程訪問主要有兩種情況:
1.不同apk(不同packageName,且不具有相同uid)的多進程:
由于linux文件權(quán)限是根據(jù)uid設置訪問權(quán)限,因此必須設置mode為MODE_WORLD_READABLE或MODE_WORLD_WRITABLE,取決于別的應用是否有需要需改該SharedPreferences,由于這種方式需要修改文件權(quán)限,且會讓所有人都具訪問權(quán)限,無法只對某個應用授權(quán),所以非常危險,android N上對targetsdk >= 24的應用已明確禁止這兩個mode,本文不再做過多解釋
2.同一個packageName或具有相同uid的package里面存在多個進程:
這種情況下多個進程具有相同的uid,因此不需要修改文件權(quán)限,沒有權(quán)限安全性問題。目前很多apk都支持多進程,例如后臺服務與前臺頁面運行在獨立的進程。這種情況是本文重點分析的。
是否需要設置MODE_MULTI_PROCESS?
先看下這個mode具體描述
/**
* SharedPreference loading flag: when set, the file on disk will
* be checked for modification even if the shared preferences
* instance is already loaded in this process. This behavior is
* sometimes desired in cases where the application has multiple
* processes, all writing to the same SharedPreferences file.
* Generally there are better forms of communication between
* processes, though.
*
* <p>This was the legacy (but undocumented) behavior in and
* before Gingerbread (Android 2.3) and this flag is implied when
* targetting such releases. For applications targetting SDK
* versions <em>greater than</em> Android 2.3, this flag must be
* explicitly set if desired.
*
* @see #getSharedPreferences
*
* @deprecated MODE_MULTI_PROCESS does not work reliably in
* some versions of Android, and furthermore does not provide any
* mechanism for reconciling concurrent modifications across
* processes. Applications should not attempt to use it. Instead,
* they should use an explicit cross-process data management
* approach such as {@link android.content.ContentProvider ContentProvider}.
*/
當設置這個參數(shù)的時候,即使當前進程內(nèi)已經(jīng)創(chuàng)建了該SharedPreferences,仍然在每次獲取的時候都會嘗試從本地文件中刷新。在同一個進程中,同一個文件只有一個實例。MODE_MULTI_PROCESS的作用
ContextImpl.java
public SharedPreferences getSharedPreferences(String name, int mode) {
//code: new instance if not exists
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)建SharedPreferences實例,若未創(chuàng)建,則先創(chuàng)建。之后判斷mode如果為MODE_MULTI_PROCESS, 則調(diào)用startReloadIfChangeUnexpectedly(),看下其實現(xiàn):
SharedPreferencesImpl.java
void startReloadIfChangedUnexpectedly() {
synchronized (this) {
// TODO: wait for any pending writes to disk?
if (!hasFileChangedUnexpectedly()) {
return;
}
startLoadFromDisk();
}
}
最終調(diào)用startLoadFromDisk()
private void startLoadFromDisk() {
synchronized (this) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
synchronized (SharedPreferencesImpl.this) {
loadFromDiskLocked();
}
}
}.start();
}
可以看出MODE_MULTI_PROCESS的作用就是在每次獲取SharedPreferences實例的時候嘗試從磁盤中加載修改過的數(shù)據(jù),并且讀取是在異步線程中,因此一個線程的修改最終會反映到另一個線程,但不能立即反映到另一個進程,所以通過SharedPreferences無法實現(xiàn)多進程同步。
綜合: 如果僅僅讓多進程可訪問同一個SharedPref文件,不需要設置MODE_MULTI_PROCESS, 如果需要實現(xiàn)多進程同步,必須設置這個參數(shù),但也只能實現(xiàn)最終一致,無法即時同步。
為什么不推薦使用MODE_MULTI_PROCESS?
android文檔已經(jīng)Deprected了這個flag,并且說明不應該通過SharedPreference做進程間數(shù)據(jù)共享?這是為啥呢?從前面但分析可看到當設置這個flag后,每次獲取(獲取而不是初次創(chuàng)建)SharedPreferences實例的時候,會判斷shared_pref文件是否修改過:
private boolean hasFileChangedUnexpectedly() {
synchronized (this) {
if (mDiskWritesInFlight > 0) {
// If we know we caused it, it's not unexpected.
if (DEBUG) Log.d(TAG, "disk write in flight, not unexpected.");
return false;
}
}
final StructStat stat;
try {
/*
* Metadata operations don't usually count as a block guard
* violation, but we explicitly want this one.
*/
BlockGuard.getThreadPolicy().onReadFromDisk();
stat = Os.stat(mFile.getPath());
} catch (ErrnoException e) {
return true;
}
synchronized (this) {
return mStatTimestamp != stat.st_mtime || mStatSize != stat.st_size;
}
}
這里先判斷mDiskWritesInFlight>0,如果成立,說明是當前進程修改了文件,不需要重新讀取。然后通過文件最后修改時間,判斷文件是否修改過。如果修改了,則重新讀取:
private void startLoadFromDisk() {
synchronized (this) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
synchronized (SharedPreferencesImpl.this) {
loadFromDiskLocked();
}
}
}.start();
}
private void loadFromDiskLocked() {
if (mLoaded) {
return;
}
if (mBackupFile.exists()) {
mFile.delete();
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);
} finally {
IoUtils.closeQuietly(str);
}
}
} catch (ErrnoException e) {
}
mLoaded = true;
if (map != null) {
mMap = map;
mStatTimestamp = stat.st_mtime;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<String, Object>();
}
notifyAll();
}
重點是這段:
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
重新讀取時,如果發(fā)現(xiàn)存在mBackupFile,則將原文件mFile刪除,并將mBackupFile重命名為mFile。mBackupFile又是如何創(chuàng)建的呢?答案是在修改SharedPreferences時將內(nèi)存中的數(shù)據(jù)寫會磁盤時創(chuàng)建的:
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)) {
mcr.setDiskWriteResult(false);
return;
}
} else {
mFile.delete();
}
}
FileOutputStream str = createFileOutputStream(mFile);
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
FileUtils.sync(str);
str.close();
ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
final StructStat stat = Os.stat(mFile.getPath());
synchronized (this) {
mStatTimestamp = stat.st_mtime;
mStatSize = stat.st_size;
}
// Writing was successful, delete the backup file if there is one.
mBackupFile.delete();
mcr.setDiskWriteResult(true);
return;
}
這段代碼只保留了核心流程,忽略了錯誤處理流程。可以看到,寫文件的步驟大致是:
- 將原文件重命名為mBackupFile
- 重新創(chuàng)建原文件mFile, 并將內(nèi)容寫入其中
- 刪除mBackupFile
所以,只有當一個進程正處于寫文件的過程中的時候,如果另一個進程讀文件,才會看到mBackupFile, 這時候讀進程會將mBackupFile重命名為mFile, 這樣讀結(jié)果是,讀進程只能都到修改前的文件,同時,由于mBackupFile重命名為了mFile, 所以寫進程寫那個文件就沒有文件名引用了,因此其寫入的內(nèi)容無法再被任何進程訪問到。所以其內(nèi)容丟失了,可認為寫入失敗了,而SharedPreferences對這種失敗情況沒有任何重試機制,所以就可能出現(xiàn)數(shù)據(jù)丟失的情況。
回到這段的重點:為什么不推薦用MODE_MULTI_PROCESS?從前面分析可知,這種模式下,每次獲取SharedPreferences都會檢測文件是否改變,只要讀的時候另一進程在寫,就會導致寫丟失。這樣失敗概率就會大幅度提高。反之,若不設置這個模式,則只在第一次創(chuàng)建SharedPreferences的時候讀取,導致寫失敗的概率就會大幅度降低,當然,仍然存在失敗的可能。
為什么不做寫失敗重試?
為毛android不做寫失敗重試呢?原因是寫進程并不能發(fā)現(xiàn)寫失敗的情況。難道寫的過程中,目標文件被刪不會拋異常嗎?答案是不會。刪除文件只是從文件系統(tǒng)中刪除了一個節(jié)點信息而已,重命名也是新建了一個具有相同名稱的節(jié)點信息,并把文件地址指向另一個磁盤地址而已,原來,之前的寫過程仍然會成功寫到原來的磁盤地址。所以目前的實現(xiàn)方案并不能檢測到失敗。
有沒有辦法解決寫失敗呢?
個人覺得是可以做到的,讀里面讀那段關(guān)鍵操作:
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
mBackupFile存在,意味著當前正處于寫讀過程中,這時候是不是可以考慮直接讀mBackupFile文件,而不刪除mFile呢?這樣讀話,讀取效果一樣,都是讀的mBackupFile,同時寫進程寫的mFile也不會被mBacupFile覆蓋,寫也就能成功了。即使通過這段代碼重命名,寫進程寫完后發(fā)現(xiàn)mBackupFile不存在了,其實也能認為發(fā)生了讀重命名,大可以重試一次。
讀文件過程中,文件被刪除會導致讀失敗嗎?
不會!與重命名一樣,文件被刪除只是刪掉節(jié)點信息,磁盤上的文件仍然存在,知道所有打開文件的fd都釋放,文件才會真正被刪除。