Since memory cannot be shared between processes, each process operates on a separate instance of SharedPreferences, which makes it unsafe for multiple processes to share data through SharedPreferences. This problem can only be solved by other means of communication between multiple processes or by using SharedPreferences without simultaneously manipulating SharedPreferences data.

Does SharedPreferences support multiple processes

{MODE_MULTI_PROCESS} {MODE_MULTI_PROCESS} {MODE_MULTI_PROCESS} {MODE_MULTI_PROCESS} {MODE_MULTI_PROCESS} {MODE_MULTI_PROCESS}Copy the code
 * @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}.
Copy the code

When using MODE_MULTI_PROCESS, it is not reliable because Android has no mechanism in place to prevent multiple processes from causing conflicts. Applications should not use MODE_MULTI_PROCESS. ContentProvider is recommended. Above, we learn that the multiple processes access to {MODE_MULTI_PROCESS} logos SharedPreferences, can lead to conflict, for example, is in the process, A clearly set A key in, jump process B to get, but prompt null error.

{MODE_WORLD_READABLE} and {MODE_WORLD_WRITEABLE} also have a lot of cross-process problems. Use SharedPreferences across processes to unlock the preferences as soon as possible, and in Android N, the system will directly report an error.

    private void checkMode(int mode) {
        if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.N) {
            if((mode & MODE_WORLD_READABLE) ! =0) {
                throw new SecurityException("MODE_WORLD_READABLE no longer supported");
            }
            if((mode & MODE_WORLD_WRITEABLE) ! =0) {
                throw new SecurityException("MODE_WORLD_WRITEABLE no longer supported"); }}}Copy the code

Now it is more and more common to have multiple processes inside the same app, so this problem needs to be addressed specifically. According to Google’s idea, it hopes to use ContentProvider to set and query data, and ContentProvider handles such multiple processes internally. So we don’t have to do synchronization operations like locking, and we focus on data access.

Refer to the SharedPreferences multi-process solution found on Baidu, most of them are using ContentProvider, and then encapsulate the operation of the database. In many cases, the function we need to use is not so complex, the basic purpose is to use the situation of cross-process can also be used, and only use ContentProvider+ database, personal feel is too heavy, generally use SharedPreferences, are simple key value pairs. Data types are inherently less complex, so using a database is overkill.

So imagine a simple solution: – This class inherits from ContentProvider, lets Google internally help us implement multi-process synchronization mechanism, saves a big trouble – want to use people (programmers), as before using SharedPreferences feel. Implements the putString(), commit() methods of the Editor so that programmers don’t have to learn how to use SharedPreferences. Do as you please – the data type is not too complex, so we don’t need the golden combination of ContentProvider+ database? In fact, with the database is just the old practice, database is powerful, can implement support more data types, if we just want simple string, Boolen,long such as, or use SharedPreferences.

ContentProvider data source

Update () and Query () are the core operations of the ContentProvider. The data sources accessed and accessed can be replaced with files, SharedPreferences, according to our needs.

The first step is to inherit the ContentProvider

Inheriting the ContentProvider, override a few of the more important methods first: OnCreate (),onUpdate(),onQuery() 1.onCreate (), which is called when the ContentProvider is initialized for the first time. Update () is triggered by a ContentResolver call to update(), which encapsulates all data operations. Insert (), delete (), and so on. 3. Query (), which is triggered when a ContentResolver calls update() and returns the survey data

    //ContentProvider requires permissions and paths
    private static String sAuthoriry;
    private static volatile Uri sAuthorityUrl;
    private UriMatcher mUriMatcher;
    private static final String KEY = "value";
    private static final String KEY_NAME = "name";
    private static final String PATH_WILDCARD = "* /";
    private static final String PATH_GET_ALL = "getAll";
    private static final String PATH_GET_STRING = "getString";
    private static final String PATH_GET_INT = "getInt";
    private static final String PATH_GET_LONG = "getLong";
    private static final String PATH_GET_FLOAT = "getFloat";
    private static final String PATH_GET_BOOLEAN = "getBoolean";
    private static final String PATH_CONTAINS = "contains";
    private static final String PATH_APPLY = "apply";
    private static final String PATH_COMMIT = "commit";
    private static final int GET_ALL = 1;
    private static final int GET_STRING = 2;
    private static final int GET_INT = 3;
    private static final int GET_LONG = 4;
    private static final int GET_FLOAT = 5;
    private static final int GET_BOOLEAN = 6;
    private static final int CONTAINS = 7;
    private static final int APPLY = 8;
    private static final int COMMIT = 9;

    @Override
    public boolean onCreate() {
        checkInitAuthority(getContext());
        mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        mUriMatcher.addURI(sAuthoriry, PATH_WILDCARD + PATH_GET_ALL, GET_ALL);
        mUriMatcher.addURI(sAuthoriry, PATH_WILDCARD + PATH_GET_STRING, GET_STRING);
        mUriMatcher.addURI(sAuthoriry, PATH_WILDCARD + PATH_GET_INT, GET_INT);
        mUriMatcher.addURI(sAuthoriry, PATH_WILDCARD + PATH_GET_LONG, GET_LONG);
        mUriMatcher.addURI(sAuthoriry, PATH_WILDCARD + PATH_GET_FLOAT, GET_FLOAT);
        mUriMatcher.addURI(sAuthoriry, PATH_WILDCARD + PATH_GET_BOOLEAN, GET_BOOLEAN);
        mUriMatcher.addURI(sAuthoriry, PATH_WILDCARD + PATH_CONTAINS, CONTAINS);
        mUriMatcher.addURI(sAuthoriry, PATH_WILDCARD + PATH_APPLY, APPLY);
        mUriMatcher.addURI(sAuthoriry, PATH_WILDCARD + PATH_COMMIT, COMMIT);
        return true;
    }Copy the code

The first step is to get the Authority string, and the second step is to pass in the UriMatcher, so that the query or update operation can match various functions. Above the onCreate() method, we define a series of strings where the PATH type represents various operations. The Uri form of the whole design is: content:// Authority/XML file name /path operation. Query SharedPreferences file name is sp, I think, for example, the operation is to obtain a String object, then the Uri can be written as: the content: / / authority/sp/get String.

For specific queries, the most important thing is to get the data, and then encapsulate the return:

public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        // The name is the name of the XML file. The default value is the first field in the PATH field of the URI
        String name = uri.getPathSegments().get(0);
        String key = null;
        String defValue = null;
        // The selectionArgs is also passed in from the client, indicating the field to be queried and the default return value for no results
        if(selectionArgs ! =null) {
            key = selectionArgs[0];
            defValue = selectionArgs[1];
        }
        /** * this is where we get the data from. By default, we open the XML file named name as context.mode_private. We then call the Preferences method for getting the value *. Once we get the data, we need to return it as a cursor, so encapsulate a BundleCursor that inherits from the MatrixCursor, */
        SharedPreferences preferences = getContext().getSharedPreferences(name, Context.MODE_PRIVATE); // Context.mode_private is used by default
        Bundle bundle = new Bundle();
        switch (mUriMatcher.match(uri)) {
            caseGET_ALL: bundle.putSerializable(KEY, (HashMap<String, ? >) preferences.getAll());break;
            case GET_STRING:
                bundle.putString(KEY, preferences.getString(key, defValue));
                break;
            case GET_INT:
                bundle.putInt(KEY, preferences.getInt(key, Integer.parseInt(defValue)));
                break;
            case GET_LONG:
                bundle.putLong(KEY, preferences.getLong(key, Long.parseLong(defValue)));
                break;
            case GET_FLOAT:
                bundle.putFloat(KEY, preferences.getFloat(key, Float.parseFloat(defValue)));
                break;
            case GET_BOOLEAN:
                bundle.putBoolean(KEY, preferences.getBoolean(key, parseBoolean(defValue)));
                break;
            case CONTAINS:
                bundle.putBoolean(KEY, preferences.contains(key));
                break;
            default:
                throw new IllegalArgumentException("This is Unknown Uri:" + uri);
        }
        return new BundleCursor(bundle);
    }Copy the code
  1. The getContext() method comes from the ContentProvider process and is used in the main process by default, so all data is processed in the same process. Avoid generating multiple SharedPreferences objects. In this way, process A does not set A value but process B obtains A null value when multiple processes access the object. Meanwhile, the internal synchronization mechanism of ContentProvider prevents multiple processes from accessing the object at the same time to avoid data conflicts.
  2. Data is returned as cursors by default. For convenience, we inherit the MatrixCursor class, override getExtras (), and return our own bundle

Effect is similar to the update (), only to realize compatible behind SharedPreferences interface, we need to when the update data to inform the corresponding listener data update (OnSharedPreferenceChangeListener), We’ll look at that later.

So far, the data update has been basically realized, and everything is ready. What east wind? Now that the entire data flow, from matching to fetching, is implemented, there are still permissions. First of all, the ContentProvider needs to be configured in the manifest.xml Manifest file, which I think everyone on earth knows, but it’s important to remember that access to this thing requires permissions.

The disclosing party:

        <provider
            android:name=".MultiProcessSharedPreferences"
            android:authorities="com.smartwork.MultiProcessSharedPreferences"
            android:exported="true"
            android:permission="com.smartwork.permission.all"
            android:readPermission="com.smartwork.read"
            android:writePermission="com.smartwork.write" />Copy the code
    <permission
        android:name="com.smartwork.read"
        android:label="provider pomission"
        android:protectionLevel="normal" />Copy the code

In addition to writing permission in the corresponding provider, export = true should also be set properly, otherwise it can only provide access to the current application or the application with the same shareDID. In addition, it is necessary to indicate that the permission is applied by your application in the periphery, and the corresponding permission will take effect. By the way, users should not forget that you need to apply for the corresponding permissions, permissions, keep a careful mind.

To use:

    <uses-permission android:name="com.smartwork.read"/>Copy the code

If not, you already have cross-process access:

                String authority = "content://com.smartwork.MultiProcessSharedPreferences";
                String PATH_WILDCARD = "/";   / / separator
                String path1 = "hello";       // The corresponding XML file name
                String path2 = "commit";      / / action

                Uri uri = Uri.parse(authority + PATH_WILDCARD + path1 + PATH_WILDCARD + path2);
                ContentValues values = new ContentValues();
                values.put("user_name"."mary");
                Cursor cursor = null;
                int result = getContentResolver().update(uri, values, null.null);Copy the code

The second step, implements SharedPreferences

After all that hard work, we can now access data from other processes using the getContentResolver().xxx method (the data source is SharedPreferences), but if we use this method for multiple processes in our own application, Users can’t be used to it. The SharedPreferences interface is the same as edit(), putString(),commit(), and SharedPreferences. The user unconsciously thinks it is the same old SharedPreferences. I’ve actually written a lot of code

Without further ado, let’s look at what this interface needs to do:

public interface SharedPreferences {

    public interface OnSharedPreferenceChangeListener {
        void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key);
    }

    public interface Editor {

        Editor putString(String key, @Nullable String value);

        Editor putStringSet(String key, @Nullable Set<String> values);

        Editor putInt(String key, int value);

        Editor putLong(String key, long value);

        Editor putFloat(String key, float value);

        Editor putBoolean(String key, boolean value);

        Editor remove(String key);

        Editor clear();

        boolean commit();

        voidapply(); } Map<String, ? > getAll();@Nullable
    String getString(String key, @Nullable String defValue);

    @Nullable
    Set<String> getStringSet(String key, @Nullable Set<String> defValues);

    int getInt(String key, int defValue);

    long getLong(String key, long defValue);

    float getFloat(String key, float defValue);

    boolean getBoolean(String key, boolean defValue);

    boolean contains(String key);

    Editor edit();

    void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);

    void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);
}Copy the code

This interface implements 3 things: 1. Basic get various data. 2. Implement register and unregister SharedPreference listeners to notify when a key changes

Various GET data

Let’s look at two of these implementations, where we can simply encapsulate our actions and simulate a ContentResolver to access data because we want to avoid creating multiple instances in an in-app multi-process.

    @SuppressWarnings("unchecked")
    @Override
    public Map<String, ?> getAll() {
        Map<String, ?> v = (Map<String, ?>) getValue(PATH_GET_ALL, null.null);
        returnv ! =null ? v : new HashMap<String.Object> (); } @Override publicString getString(String key, String defValue) {
        return (String) getValue(PATH_GET_STRING, key, defValue);
    }
Copy the code
private Object getValue(String pathSegment, String key, Object defValue) {
        Object v = null;
        if (mIsSafeMode) { // If the device is in safe mode, return defValue;
            return defValue;
        }
        try {
            checkInitAuthority(mContext);
        } catch (RuntimeException e) { / / to solve collapse: Java. Lang. RuntimeException: Package manager has died at android.app.ApplicationPackageManager.getPackageInfo(ApplicationPackageManager.java:77)
            if (isPackageManagerHasDied(e)) {
                return defValue;
            } else {
                throw e;
            }
        }
        Uri uri = Uri.withAppendedPath(Uri.withAppendedPath(sAuthorityUrl, mName), pathSegment);
        String[] selectionArgs = new String[]{key, defValue == null ? null : String.valueOf(defValue)};
        Cursor cursor = null;
        try {
            cursor = mContext.getContentResolver().query(uri, null.null, selectionArgs, null);
        } catch (SecurityException e) { / / to solve collapse: Java. Lang. SecurityException: Permission ": reading uri content://xxx from pid=2446, uid=10116 requires the provider be exported, or grantUriPermission() at android.content.ContentProvider$Transport.enforceReadPermission(ContentProvider.java:332) ...
            if(DEBUG) { e.printStackTrace(); }}catch (RuntimeException e) { / / to solve collapse: Java. Lang. RuntimeException: Package manager has died at android.app.ApplicationPackageManager.resolveContentProvider(ApplicationPackageManager.java:609) ... at android.content.ContentResolver.query(ContentResolver.java:404)
            if (isPackageManagerHasDied(e)) {
                return defValue;
            } else {
                throwe; }}if(cursor ! =null) {
            Bundle bundle = null;
            try {
                bundle = cursor.getExtras();
            } catch (RuntimeException e) { / / solve the ContentProvider is killed in the process of the exception thrown: Java. Lang. RuntimeException: android.os.DeadObjectException at android.database.BulkCursorToCursorAdaptor.getExtras(BulkCursorToCursorAdaptor.java:173) at android.database.CursorWrapper.getExtras(CursorWrapper.java:94)
                if(DEBUG) { e.printStackTrace(); }}if(bundle ! =null) {
                v = bundle.get(KEY);
                bundle.clear();
            }
            cursor.close();
        }
        returnv ! =null ? v : defValue;
    }Copy the code

GetValue () is the core of various get order operation, from the front to get us the path of the operation, need access to the key value and the default value, the last and mContext. GetContentResolver () query, run to this aspect, in the middle of the process, Determines whether it is currently in safe mode (safe mode, custom ContentProvider is not available), initializes Authority, and catches various runtime exceptions.

The realization of the Editor

Do this better than the above, in fact the system native {SharedPreferencesImpl} there is a detailed implementation, we use can be moved by his form, only need to modify the commit (), and other key parts of the code can, to access to ContentProvider.

    public final class EditorImpl implements Editor {
        private final Map<String, Object> mModified = new HashMap<String, Object>();
        private boolean mClear = false;

        @Override
        public Editor putString(String key, String value) {
            synchronized (this) {
                mModified.put(key, value);
                return this; }}@Override
        public Editor putStringSet(String key, Set<String> values) {
            synchronized (this) {
                mModified.put(key, (values == null)?null : new HashSet<String>(values));
                return this; }}@Override
        public Editor putInt(String key, int value) {
            synchronized (this) {
                mModified.put(key, value);
                return this; }}@Override
        public Editor putLong(String key, long value) {
            synchronized (this) {
                mModified.put(key, value);
                return this; }}@Override
        public Editor putFloat(String key, float value) {
            synchronized (this) {
                mModified.put(key, value);
                return this; }}@Override
        public Editor putBoolean(String key, boolean value) {
            synchronized (this) {
                mModified.put(key, value);
                return this; }}@Override
        public Editor remove(String key) {
            synchronized (this) {
                mModified.put(key, null);
                return this; }}@Override
        public Editor clear() {
            synchronized (this) {
                mClear = true;
                return this; }}@Override
        public void apply() {
            setValue(PATH_APPLY);
        }

        @Override
        public boolean commit() {
            return setValue(PATH_COMMIT);
        }

        private boolean setValue(String pathSegment) {
            boolean result = false;
            if (mIsSafeMode) { // If the device is in safe mode, return false;
                return result;
            }
            try {
                checkInitAuthority(mContext);
            } catch (RuntimeException e) { / / to solve collapse: Java. Lang. RuntimeException: Package manager has died at android.app.ApplicationPackageManager.getPackageInfo(ApplicationPackageManager.java:77)
                if (isPackageManagerHasDied(e)) {
                    return result;
                } else {
                    throw e;
                }
            }
            String[] selectionArgs = new String[]{String.valueOf(mClear)};
            synchronized (this) {
                Uri uri = Uri.withAppendedPath(Uri.withAppendedPath(sAuthorityUrl, mName), pathSegment);
                ContentValues values = ReflectionUtil.contentValuesNewInstance((HashMap<String, Object>) mModified);
                try {
                    result = mContext.getContentResolver().update(uri, values, null, selectionArgs) > 0;
                } catch (IllegalArgumentException e) { / / solve the ContentProvider is killed in the process of the exception thrown: Java. Lang. IllegalArgumentException: Unknown URI content://xxx.xxx.xxx/xxx/xxx at android.content.ContentResolver.update(ContentResolver.java:1312)
                    if(DEBUG) { e.printStackTrace(); }}catch (RuntimeException e) { / / to solve collapse: Java. Lang. RuntimeException: Package manager has died at android.app.ApplicationPackageManager.resolveContentProvider(ApplicationPackageManager.java:609) ... at android.content.ContentResolver.update(ContentResolver.java:1310)
                    if (isPackageManagerHasDied(e)) {
                        return result;
                    } else {
                        throwe; }}finally {
                    mModified.clear();
                    mClear = false; }}returnresult; }}Copy the code

The most important part of the above code is the setValue() method. The mModified object holds the modified key-value pair. We store the map in the ContentValues constructor and update the method directly. In this way, the operation of storing data moves from SharedPreferences to ContentProvider.

You see this ReflectionUtil. ContentValuesNewInstance ((HashMap

 @SuppressWarnings("unchecked")
    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        int result = 0;
        String name = uri.getPathSegments().get(0);
        /** * Data source or getSharedPreferences */
        SharedPreferences preferences = getContext().getSharedPreferences(name, Context.MODE_PRIVATE);
        int match = mUriMatcher.match(uri);
        switch (match) {
            case APPLY:
            case COMMIT:
                booleanhasListeners = mListeners ! =null && mListeners.size() > 0;
                ArrayList<String> keysModified = null;
                Map<String, Object> map = null;
                if (hasListeners) {
                    keysModified = new ArrayList<String>();
                    map = (Map<String, Object>) preferences.getAll();
                }
                Editor editor = preferences.edit();
                /** * this clear is the value received by setValue() above, if true, editor.clear() */
                boolean clear = TextUtils.isEmpty(selectionArgs[0])?null : Boolean.parseBoolean(selectionArgs[0]);
                if (clear) {
                    if(hasListeners && ! map.isEmpty()) {for (Map.Entry<String, Object> entry : map.entrySet()) {
                            keysModified.add(entry.getKey());
                        }
                    }
                    editor.clear();
                }
                for (Map.Entry<String, Object> entry : values.valueSet()) {
                    String k = entry.getKey();
                    Object v = entry.getValue();
                    if (v instanceof EditorImpl || v == null) {
                        editor.remove(k);
                        if(hasListeners && map.containsKey(k)) { keysModified.add(k); }}else {
                        if(hasListeners && (! map.containsKey(k) || (map.containsKey(k) && ! v.equals(map.get(k))))) { keysModified.add(k); }}if (v instanceof String) {
                        editor.putString(k, (String) v);
                    } else if (v instanceof Set) {
                        edit().putStringSet(k, (Set<String>) v);
                    } else if (v instanceof Integer) {
                        editor.putInt(k, (Integer) v);
                    } else if (v instanceof Long) {
                        editor.putLong(k, (Long) v);
                    } else if (v instanceof Float) {
                        editor.putFloat(k, (Float) v);
                    } else if (v instanceofBoolean) { editor.putBoolean(k, (Boolean) v); }}/** * notifyListeners */ are notified by calling the corresponding methods apply() or commit() *
                switch (match) {
                    case APPLY:
                        editor.apply();
                        result = 1;
                        notifyListeners(name, keysModified);
                        break;
                    case COMMIT:
                        if (editor.commit()) {
                            result = 1;
                            notifyListeners(name, keysModified);
                        }
                        break;
                    default:
                        break;
                }

                values.clear();
                break;
            default:
                throw new IllegalArgumentException("This is Unknown Uri:" + uri);
        }
        return result;
    }Copy the code

The implementation of the listener compares the listener, using a WeakHashMap to store all the listeners, WeahHashMap will remove the empty content when updating.

At this point, the MultiProcessSharedPreferences is finished code source address: SharedPreference youdao note

conclusion

In fact, the whole idea is that people within their own projects, when this class is used as a normal SharedPreference, and can be implemented across processes, a small change in the middle is to inherit the ContentProvider, and then the data source is still SharedPreference. The whole feeling is – internal use: SharedPreference -> ContentProvider -> SharedPreference data source – external use: ContentProvider -> SharedPreference data source

== uses ContentProvider as an intermediary to implement cross-process access.==