Android SharedPreference supports multi-process
When using SharedPreference, there are some modes as follows: MODE_PRIVATE
Private mode, this is the most common mode, and this mode is generally used. MODE_WORLD_READABLE
, MODE_WORLD_WRITEABLE
, the file has open read and write permissions, it is not safe, and has been abandoned. Google recommends using FileProvider
shared files. MODE_MULTI_PROCESS
, cross-process mode, if the project has multiple processes using the same Preference, you need to use this mode, but it has been abandoned, see the following description
/**
* 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 does not guarantee that this mode will always work correctly, and it is recommended to use ContentProvider
an alternative. Combined with the previous flags, it can be found that Google believes that it is unsafe for multiple processes MODE_WORLD_READABLE
to read the same file, and it is not recommended to do so . It's actually a principle:ContentProivder
FileProvider
ContentProvider
Make sure that only one process is reading and writing to a file
Why is MODE_MULTI_PROCESS not recommended
The reason is not complicated. We can look at it from the android source code. context.getSharedPreferences
The class obtained by the method is essentially SharedPreferencesImpl
. This class is a simple second-level cache, which loads all the data in the file into memory at startup.
private void startLoadFromDisk() {
synchronized (this) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
It is also reminded here that since the SharedPreference
content will be stored in memory, do not use to SharedPreference
save larger content to avoid unnecessary memory waste.
Note that there is a lock mLoaded
, when SharedPreference
doing other operations, you must wait for the lock to be released
@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
模式,其他的模式比较少见,基本没怎么用。 - In the cross-process
SharedPreferenceProxy
,registerOnSharedPreferenceChangeListener
it has not been implemented yet, you can use theContentObserver
implementation of cross-process monitoring.
For the detailed code, see: https://github.com/liyuanhust/MultiprocessPreference
Reprinted from: https://www.jianshu.com/p/875d13458538