Android SharedPreference supports multi-process

Android SharedPreference supports multi-process

96 
other 
2017.03.28 00:35*   Word Count 1345   Read 1999 Comments 1

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 FileProvidershared 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 ContentProvideran alternative. Combined with the previous flags, it can be found that Google believes that it is unsafe for multiple processes MODE_WORLD_READABLEto read the same file, and it is not recommended to do so . It's actually a principle:ContentProivderFileProviderContentProvider

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 SharedPreferencecontent will be stored in memory, do not use to SharedPreferencesave larger content to avoid unnecessary memory waste.

Note that there is a lock mLoaded , when SharedPreferencedoing 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 applyis asynchronous. The memory is written first, and then the file is written in the asynchronous thread. applyIt 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_PROCESSIn 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_PROCESSthat 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!

  1. When using MODE_MULTI_PROCESS, do not save the SharedPreference variable, it must be obtained from each time context.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.
  2. 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.
  3. 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_PROCESSis 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

  1. 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.
  2. After most of the programs are ContentProvidercompleted, all calls are gone ContentProvider. However, if the calling process SharedPreference is the same process as itself, just use the native process, and you don't need to turn around to access ContentProviderit, 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 SharedPreferenceProvidermethods 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 SharedPreferenceProvidersame. 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 SharedPreferencesit, don't care whether it is obtained SharedPreferenceProxyor not SharedPreferenceImpl. Even if you are not currently involved in multi-process use, SharedPreferenceafter 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:

  1. SharedPreferencesAll modes are used for acquisition MODE_PRIVATE, and other modes are relatively rare and basically useless.
  2. In the cross-process SharedPreferenceProxy , registerOnSharedPreferenceChangeListenerit has not been implemented yet, you can use the ContentObserverimplementation of cross-process monitoring.

For the detailed code, see: https://github.com/liyuanhust/MultiprocessPreference

Reprinted from: https://www.jianshu.com/p/875d13458538

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325589134&siteId=291194637