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;
}
}
There are two write operations commit
apply
. commit
It is synchronous. Colleagues writing to the memory will wait for the completion of the writing file. It apply
is asynchronous. The memory is written first, and then the file is written in the asynchronous thread. apply
It must be faster. It is recommended to use apply
how SharedPreferenceImpl is created. In the ContextImpl class
@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;
}
In this code, we can see that 1. SharedPreferencesImpl is stored in the global map cache and will only be created once. 2. MODE_MULTI_PROCESS
In mode, every time you get it, you will try to read the file reload. Of course, there will be some logic to minimize the number of reads, such as whether there is currently an ongoing read operation, whether the modification time and size of the file have changed from the last time, etc. It turns out MODE_MULTI_PROCESS
that this is how to ensure that the multi-process data is correct!
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;
}
}
There are at least 3 pits here!
- When using
MODE_MULTI_PROCESS
, do not save the SharedPreference variable, it must be obtained from each timecontext.getSharedPreferences
. If you save it as a convenient variable, then reload cannot be triggered, and the data of the two processes may be out of sync. - As mentioned earlier, loading data is time-consuming, and other operations will wait for the lock. This means that in many cases, the SharedPreference data has to be read from the file again, which greatly reduces the effect of the memory cache. File read and write time also affects performance.
- Useful when modifying data
commit
, to ensure that the file is written during modification, so that other processes can perceive it through the file size or modification time.
To sum up, no matter what you say, it MODE_MULTI_PROCESS
is bad, and it is right to avoid it.
Multi-process use SharedPreference scheme
It's simple and simple, just use it according to google's suggestions ContentProvider
. I've seen a lot of examples online, but always feel like something's missing
- In some solutions, all read operations are written as static methods without inheritance
SharedPreference
. Doing so requires forcing a change in the caller's usage habits, which is not very good. - After most of the programs are
ContentProvider
completed, all calls are goneContentProvider
. However, if the calling processSharedPreference
is the same process as itself, just use the native process, and you don't need to turn around to accessContentProvider
it, reducing unnecessary performance loss.
I also wrote a cross-process solution here, briefly introducing SharedPreferenceProxy
inheritance as follows SharedPreferences
. All its operations are done through ContentProvider. Brief code:
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 is just a class that encapsulates Bundle operations.
All cross-process operations are done through SharedPreferenceProvider
methods call
. SharedPreferenceProvider will access the realSharedPreference
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);
}
...
}
The important difference is here: when calling getSharedPreferences
, it will first determine whether the caller's process pid is the SharedPreferenceProvider
same. If different, return SharedPreferenceProxy
. If the same, return ctx.getSharedPreferences
. It will only be judged when it is called for the first time, and the result will be saved.
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);
}
This way, only go if the caller is truly cross-process contentProvider
. For the case of the same process, there is no need to go contentProvider
. For the caller, this is all transparent, just need to get SharedPreferences
it, don't care whether it is obtained SharedPreferenceProxy
or not SharedPreferenceImpl
. Even if you are not currently involved in multi-process use, SharedPreference
after encapsulating and replacing all the acquired places, it will have no effect on the current logic.
public static SharedPreferences getSharedPreference(@NonNull Context ctx, String preferName) {
return SharedPreferenceProxy.getSharedPreferences(ctx, preferName);
}
</br>
Note two points:
SharedPreferences
All modes are used for acquisitionMODE_PRIVATE
, and other modes are relatively rare and basically useless.- 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