Android SharedPreference 支持多进程

xiaoxiao2021-02-28  22

Android SharedPreference 支持多进程

  Lainn  关注 2017.03.28 00:35*  字数 1345  阅读 1999 评论 1

在使用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肯定要快一些,优先推荐使用applySharedPreferenceImpl是如何创建的呢,在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

转载请注明原文地址: https://www.6miu.com/read-2631901.html

最新回复(0)