Android SharedPreference 支持多進程
在使用SharedPreference 時,有如下一些模式:MODE_PRIVATE
私有模式,這是最常見的模式,一般情況下都使用該模式。 MODE_WORLD_READABLE
,MODE_WORLD_WRITEABLE
,文件開放讀寫權限,不安全,已經被廢棄了,google建議使用FileProvider
共享文件。MODE_MULTI_PROCESS
,跨進程模式,如果項目有多個進程使用同一個Preference,需要使用該模式,但是也已經廢棄了,見如下說明
/**
* 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.
*
* @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}.
*/
Android不保證該模式總是能正確的工作,建議使用ContentProvider
替代。結合前面的MODE_WORLD_READABLE
標誌,可以發現,Google認爲多個進程讀同一個文件都是不安全的,不建議這麼做,推薦使用ContentProivder
來處理多進程間的文件共享,FileProvider
也繼承於ContentProvider
。實際上就是一條原則:
確保一個文件只有一個進程在讀寫操作
爲什麼不建議使用MODE_MULTI_PROCESS
原因並不複雜,我們可以從android源碼看一下,通過方法context.getSharedPreferences
獲取到的類實質上是SharedPreferencesImpl
。該類就是一個簡單的二級緩存,在啓動時會將文件裏的數據全部都加載到內存裏,
private void startLoadFromDisk() {
synchronized (this) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
這裏也提醒一下,由於SharedPreference
內容都會在內存裏存一份,所以不要使用SharedPreference
保存較大的內容,避免不必要的內存浪費。
注意有一個鎖mLoaded
,在對SharedPreference
做其他操作時,都必須等待該鎖釋放
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (this) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
寫操作有兩個commit
apply
。 commit
是同步的,寫入內存的同事會等待寫入文件完成,apply
是異步的,先寫入內存,在異步線程裏再寫入文件。apply
肯定要快一些,優先推薦使用apply
SharedPreferenceImpl是如何創建的呢,在ContextImpl類裏
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
checkMode(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;
}
}
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;
}
這段代碼裏,我們可以看出,1. SharedPreferencesImpl是保存在全局個map cache裏的,只會創建一次。2,MODE_MULTI_PROCESS
模式下,每次獲取都會嘗試去讀取文件reload。當然會有一些邏輯儘量減少讀取次數,比如當前是否有正在進行的讀取操作,文件的修改時間和大小與上次有沒有變化等。原來MODE_MULTI_PROCESS
是這樣保證多進程數據正確的!
void startReloadIfChangedUnexpectedly() {
synchronized (this) {
// TODO: wait for any pending writes to disk?
if (!hasFileChangedUnexpectedly()) {
return;
}
startLoadFromDisk();
}
}
// Has the file changed out from under us? i.e. writes that
// we didn't instigate.
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;
}
}
這裏起碼有3個坑!
- 使用
MODE_MULTI_PROCESS
時,不要保存SharedPreference變量,必須每次都從context.getSharedPreferences
獲取。如果你圖方便使用變量存了下來,那麼無法觸發reload,有可能兩個進程數據不同步。 - 前面提到過,load數據是耗時的,並且其他操作會等待該鎖。這意味着很多時候獲取SharedPreference數據都不得不從文件再讀一遍,大大降低了內存緩存的作用。文件讀寫耗時也影響了性能。
- 修改數據時得用
commit
,保證修改時寫入了文件,這樣其他進程才能通過文件大小或修改時間感知到。
綜上,無論怎麼說,MODE_MULTI_PROCESS
都很糟糕,避免使用就對了。
多進程使用SharedPreference方案
說簡單也簡單,就是依據google的建議使用ContentProvider
了。我看過網上很多的例子,但總是覺得少了點什麼
- 有的方案裏將所有讀取操作都寫作靜態方法,沒有繼承
SharedPreference
。 這樣做需要強制改變調用者的使用習慣,不怎麼好。 - 大部分方案做成
ContentProvider
後,所有的調用都走的ContentProvider
。但如果調用進程與SharedPreference
本身就是同一個進程,只用走原生的流程就行了,不用拐個彎去訪問ContentProvider
,減少不必要的性能損耗。
我這裏也寫了一個跨進程方案,簡單介紹如下SharedPreferenceProxy
繼承SharedPreferences
。其所有操作都是通過ContentProvider完成。簡要代碼:
public class SharedPreferenceProxy implements SharedPreferences {
@Nullable
@Override
public String getString(String key, @Nullable String defValue) {
OpEntry result = getResult(OpEntry.obtainGetOperation(key).setStringValue(defValue));
return result == null ? defValue : result.getStringValue(defValue);
}
@Override
public Editor edit() {
return new EditorImpl();
}
private OpEntry getResult(@NonNull OpEntry input) {
try {
Bundle res = ctx.getContentResolver().call(PreferenceUtil.URI
, PreferenceUtil.METHOD_QUERY_VALUE
, preferName
, input.getBundle());
return new OpEntry(res);
} catch (Exception e) {
e.printStackTrace();
return null;
}
...
public class EditorImpl implements Editor {
private ArrayList<OpEntry> mModified = new ArrayList<>();
@Override
public Editor putString(String key, @Nullable String value) {
OpEntry entry = OpEntry.obtainPutOperation(key).setStringValue(value);
return addOps(entry);
}
@Override
public void apply() {
Bundle intput = new Bundle();
intput.putParcelableArrayList(PreferenceUtil.KEY_VALUES, convertBundleList());
intput.putInt(OpEntry.KEY_OP_TYPE, OpEntry.OP_TYPE_APPLY);
try {
ctx.getContentResolver().call(PreferenceUtil.URI, PreferenceUtil.METHOD_EIDIT_VALUE, preferName, intput);
} catch (Exception e) {
e.printStackTrace();
}
...
}
...
}
OpEntry只是一個對Bundle操作封裝的類。
所有跨進程的操作都是通過SharedPreferenceProvider
的call
方法完成。SharedPreferenceProvider裏會訪問真正的SharedPreference
public class SharedPreferenceProvider extends ContentProvider{
private Map<String, MethodProcess> processerMap = new ArrayMap<>();
@Override
public boolean onCreate() {
processerMap.put(PreferenceUtil.METHOD_QUERY_VALUE, methodQueryValues);
processerMap.put(PreferenceUtil.METHOD_CONTAIN_KEY, methodContainKey);
processerMap.put(PreferenceUtil.METHOD_EIDIT_VALUE, methodEditor);
processerMap.put(PreferenceUtil.METHOD_QUERY_PID, methodQueryPid);
return true;
}
@Nullable
@Override
public Bundle call(@NonNull String method, @Nullable String arg, @Nullable Bundle extras) {
MethodProcess processer = processerMap.get(method);
return processer == null?null:processer.process(arg, extras);
}
...
}
重要差別的地方在這裏:在調用getSharedPreferences
時,會先判斷caller的進程pid是否與SharedPreferenceProvider
相同。如果不同,則返回SharedPreferenceProxy
。如果相同,則返回ctx.getSharedPreferences
。只會在第一次調用時進行判斷,結果會保存起來。
public static SharedPreferences getSharedPreferences(@NonNull Context ctx, String preferName) {
//First check if the same process
if (processFlag.get() == 0) {
Bundle bundle = ctx.getContentResolver().call(PreferenceUtil.URI, PreferenceUtil.METHOD_QUERY_PID, "", null);
int pid = 0;
if (bundle != null) {
pid = bundle.getInt(PreferenceUtil.KEY_VALUES);
}
//Can not get the pid, something wrong!
if (pid == 0) {
return getFromLocalProcess(ctx, preferName);
}
processFlag.set(Process.myPid() == pid ? 1 : -1);
return getSharedPreferences(ctx, preferName);
} else if (processFlag.get() > 0) {
return getFromLocalProcess(ctx, preferName);
} else {
return getFromRemoteProcess(ctx, preferName);
}
}
private static SharedPreferences getFromRemoteProcess(@NonNull Context ctx, String preferName) {
synchronized (SharedPreferenceProxy.class) {
if (sharedPreferenceProxyMap == null) {
sharedPreferenceProxyMap = new ArrayMap<>();
}
SharedPreferenceProxy preferenceProxy = sharedPreferenceProxyMap.get(preferName);
if (preferenceProxy == null) {
preferenceProxy = new SharedPreferenceProxy(ctx.getApplicationContext(), preferName);
sharedPreferenceProxyMap.put(preferName, preferenceProxy);
}
return preferenceProxy;
}
}
private static SharedPreferences getFromLocalProcess(@NonNull Context ctx, String preferName) {
return ctx.getSharedPreferences(preferName, Context.MODE_PRIVATE);
}
這樣,只有當調用者是正真跨進程時才走的contentProvider
。對於同進程的情況,就沒有必要走contentProvider
了。對調用者來說,這都是透明的,只需要獲取SharedPreferences
就行了,不用關心獲得的是SharedPreferenceProxy
,還是SharedPreferenceImpl
。即使你當前沒有涉及到多進程使用,將所有獲取SharedPreference
的地方封裝並替換後,對當前邏輯也沒有任何影響。
public static SharedPreferences getSharedPreference(@NonNull Context ctx, String preferName) {
return SharedPreferenceProxy.getSharedPreferences(ctx, preferName);
}
</br>
注意兩點:
- 獲取
SharedPreferences
使用的都是MODE_PRIVATE
模式,其他的模式比較少見,基本沒怎麼用。 - 在跨進程的
SharedPreferenceProxy
裏,registerOnSharedPreferenceChangeListener
暫時還沒有實現,可以使用ContentObserver
實現跨進程監聽。
詳細代碼見:https://github.com/liyuanhust/MultiprocessPreference
轉自:https://www.jianshu.com/p/875d13458538