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
实现跨进程监听。