SharedPreferences 多进程解决方案

14,031 阅读13分钟
原文链接: blog.csdn.net

由于进程间是不能内存共享的,每个进程操作的SharedPreferences都是一个单独的实例,这导致了多进程间通过SharedPreferences来共享数据是不安全的,这个问题只能通过多进程间其它的通信方式或者是在确保不会同时操作SharedPreferences数据的前提下使用SharedPreferences来解决。

SharedPreferences支持多进程吗

其实原则上是不支持的,下面这段话是android对字段{MODE_MULTI_PROCESS}的注释:
 * @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}.

当使用MODE_MULTI_PROCESS这个字段时,其实并不可靠,因为Android内部并没有合适的机制去防止多个进程所造成的冲突,应用不应该使用它,推荐使用ContentProvider。上面这段介绍我们得知:多个进程访问{MODE_MULTI_PROCESS}标识的SharedPreferences时,会造成冲突,举个例子就是,在A进程,明明set了一个key进去,跳到B进程去取,却提示null的错误。

刚才笔者无聊(其实想自己验证一下,尝试了SharedPreferences其他mode,效果惨不忍睹,{MODE_WORLD_READABLE}和{MODE_WORLD_WRITEABLE}跨进程也有一堆问题,所以想跨进程用SharedPreferences,尽早脱坑,而且在Android N版本,系统会直接报错的)

    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");
            }
        }
    }

现在同一个app内部多进程也越来越常见了,所以这个问题还是需要针对性解决的,按google的想法,是希望通过ContentProvider去set,query数据,ContentProvider内部对这种多进程做了处理,这样我们就不用去做加锁这类的同步操作,将我们的核心思想放在数据存取上了。

参考过百度上搜索出来的SharedPreferences多进程解决方案,比较多的都是采用ContentProvider,然后封装一下数据库的操作。其实很多情况下,我们所需要使用的功能并不是那么复杂,根本目的就是想跨进程的情况下还能使用,而且只使用ContentProvider+数据库,个人感觉还是太重了,一般使用SharedPreferences,都是简单的键值对,数据类型本来就不会太复杂的,用数据库岂不是大材小用了。

所以设想也很简单,这个解决方案:
- 这个类继承于ContentProvider,让google内部帮我们实现多进程同步机制,省了一个大麻烦
- 希望使用的人(程序员),还是当以往使用SharedPreferences的感觉去操作,我意思是implements SharedPreferences这个接口,把里面的Editor的putString(),commit()等方法实现好,这样程序员就不用学习这个SharedPreferences究竟要怎么用了。以前该干嘛就干嘛
- 数据类型不会太复杂,那么我们不一定要ContentProvider+数据库的黄金组合啊?其实搭配数据库只是以前的做法,数据库功能强大,可以实现支持的数据类型也更多,如果我们只是希望简单的string,boolen,long这种,还是用回SharedPreferences。

ContentProvider数据源

其实细心思考一下,ContentProvider并不是只能选择数据库,其实核心操作就在update()和query()这两个操作,里面操作存取的数据源其实可以根据我们需要,替换成文件,SharedPreferences。

第一步,继承ContentProvider

继承ContentProvider,首先重载几个比较重要的方法:onCreate(),onUpdate(),onQuery()
1. onCreate(),第一次ContentProvider初始化的时候会调用,一般进行基本的Uri匹配类型初始化。
2. update(),当有ContentResolver调用update()方法时候会触发,进行更新操作,其实我们对数据的相关操作,都可以封装在这个方法里面,所以这次只重载这个方法,其他的方法还有insert(),delete()等等。
3. query(),当有ContentResolver调用update()方法时候会触发,返回调查的数据

    //ContentProvider所需要要权限、路径
    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;
    }

我们先看onCreate()方法,在这个方法里面,我们主要有2个操作,第一步获取Authority的字符串,第二步new UriMatcher,把对应的Uri传递进去,方便后面进行查询或更新操作的时候匹配各种功能,在onCreate()方法上面,我们定义了一系列的字符串,其中PATH类型代表各种操作。整个设计的Uri形式是:content://authority/操作的xml文件名/path操作。比如我想查询SharedPreferences文件名是sp,操作是获取一个String对象,那么Uri可以写成:content://authority/sp/getString。

具体查询的时候,最重要的是获取到数据,然后封装好返回:

public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        //这个name 获取的就是xml的文件名,默认取uri的path字段的第一个
        String name = uri.getPathSegments().get(0);
        String key = null;
        String defValue = null;
        //这个selectionArgs也是从客户端传递进来,表明想要查询的字段以及查无结果默认的返回值
        if (selectionArgs != null) {
            key = selectionArgs[0];
            defValue = selectionArgs[1];
        }
        /**
         * 这个是我们获取数据的来源,默认是以Context.MODE_PRIVATE的形式打开名为name的xml文档
         * 下面会根据当前查询的Uri的地址匹配不同的方法,然后去调用preferences对应获取value的方法
         * 拿到数据后,我们需要以游标的形式返回,所以封装了一个BundleCursor,继承于MatrixCursor,
         */
        SharedPreferences preferences = getContext().getSharedPreferences(name, Context.MODE_PRIVATE); //默认采用Context.MODE_PRIVATE
        Bundle bundle = new Bundle();
        switch (mUriMatcher.match(uri)) {
            case GET_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);
    }
  1. 数据来源就是getContext().getSharedPreferences()方法,getContext()方法来自于ContentProvider,默认进行在主进程,所以整个对数据的操作都是在同一个进程里面进行,避免产生多个SharedPreferences对象,这样多进程访问的时候就不会出现在A进程set了值但是B进程获取到的是null的问题,同时,ContentProvider内部的同步机制会防止多个进程同时访问,避免数据冲突。
  2. 数据的组成形式默认是以游标的形式返回,为了方便,我们直接继承原来的MatrixCursor类,重写getExtras()方法,返回我们自己组装的bundle对象即可

update()方面效果类似,只是为了兼容后面实现的SharedPreferences接口,我们需要在update数据的时候通知对应的监听器数据更新(OnSharedPreferenceChangeListener),这个方面我们放到后面一起看。

到目前为止,数据的更新已经基本实现,万事俱备只欠东风。什么东风呢?现在整个数据流程,从匹配到获取都实现了,剩下的还有权限问题,首先这个ContentProvider需要在Manifest.xml清单文件配置,这我想地球人都知道,但是我们还是要记得这个东西的访问是需要权限的。

提供方:

        <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" />
    <permission
        android:name="com.smartwork.read"
        android:label="provider pomission"
        android:protectionLevel="normal" />

权限方面,大家写的时候还要小心,除了在对应的provider里面写权限之外,export = true也要设置好,否则只能提供当前应用或者同一个sharedid的应用访问,另外还需要在外围表明这个权限是你这个应用申请的,对应的权限才生效。对了,使用方别忘记你自己是需要申请对应的权限的,权限问题,多多留个心眼。

使用方:

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

如无意外,你已经可以跨进程访问了:

                String authority = "content://com.smartwork.MultiProcessSharedPreferences";
                String PATH_WILDCARD = "/";   //分割符
                String path1 = "hello";       //对应的xml文件名
                String path2 = "commit";      //动作

                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);

第二步,implements SharedPreferences

经过上面辛勤的劳动后,我们已经可以从别的进程采用getContentResolver().xxx操作的方法进行访问别人进程的数据了(数据源是SharedPreferences),但是如果我们自己应用的多进程也是这样的访问方式,使用者肯定不习惯啊。好吧,转了个思维,我们在上面的基础上,实现这个SharedPreferences接口,那么用起来还是以前的edit()拿编辑器,putString()放数据,commit()提交数据,让使用者不知不觉以为还是以前的SharedPreferences。其实我已经写了很多代码了····

废话不多说,先看看这个接口需要实现哪些功能:

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();

        void apply();
    }

    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);
}

这段有点长,大家听我说,这个接口就是实现3样东西:
1. 基本的get各种各样的数据
2. editor封装,里面做平时的put各种操作
3. 实现注册和反注册SharedPreference监听器,key有变化的时候就通知

各种get数据

我们看其中两个实现,由于在应用内部多进程里面也要避免创建多个实例,所以我们在这里可以简单封装自己的操作,模拟ContentResolver进行访问数据。

    @SuppressWarnings("unchecked")
    @Override
    public Map<String, ?> getAll() {
        Map<String, ?> v = (Map<String, ?>) getValue(PATH_GET_ALL, null, null);
        return v != null ? v : new HashMap<String, Object>();
    }

    @Override
    public String getString(String key, String defValue) {
        return (String) getValue(PATH_GET_STRING, key, defValue);
    }
private Object getValue(String pathSegment, String key, Object defValue) {
        Object v = null;
        if (mIsSafeMode) { // 如果设备处在“安全模式”,返回defValue;
            return defValue;
        }
        try {
            checkInitAuthority(mContext);
        } catch (RuntimeException e) { // 解决崩溃: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) { // 解决崩溃:java.lang.SecurityException: Permission Denial: 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) { // 解决崩溃: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 {
                throw e;
            }
        }
        if (cursor != null) {
            Bundle bundle = null;
            try {
                bundle = cursor.getExtras();
            } catch (RuntimeException e) { // 解决ContentProvider所在进程被杀时的抛出的异常: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();
        }
        return v != null ? v : defValue;
    }

getValue()里面就是各种get实现的核心操作,从前面我们拿到的path操作,需要访问的key的值和默认值,最后也是mContext.getContentResolver().query,跑到这个方面里面,中间的过程中,会判断一下当前是否安全模式(安全模式,自定义ContentProvider无法使用),初始化Authority,以及catch了各种运行时异常。

Editor 的实现

这个比上面更好做,其实系统原生的{SharedPreferencesImpl}就有详细的实现,我们可以按照他的形式搬过来使用,只需要修改commit()等关键地方的代码就可以,改成对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) { // 如果设备处在“安全模式”,返回false;
                return result;
            }
            try {
                checkInitAuthority(mContext);
            } catch (RuntimeException e) { // 解决崩溃: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) { // 解决ContentProvider所在进程被杀时的抛出的异常: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) { // 解决崩溃: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 {
                        throw e;
                    }
                } finally {
                    mModified.clear();
                    mClear = false;
                }
            }
            return result;
        }
    }

上面的代码最关键的地方就是setValue()方法,mModified对象存放着修改后的键值对,我们通过ContentValues构造方法将map存进去,然后直接update方法就可以了。这样子存数据的操作又从SharedPreferences转到ContentProvider。

大家看到这个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);
        /**
         * 数据源还是getSharedPreferences
         */
        SharedPreferences preferences = getContext().getSharedPreferences(name, Context.MODE_PRIVATE);
        int match = mUriMatcher.match(uri);
        switch (match) {
            case APPLY:
            case COMMIT:
                boolean hasListeners = 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();
                /**
                 *  这个clear就是上面setValue()的时候接收的值,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 instanceof Boolean) {
                        editor.putBoolean(k, (Boolean) v);
                    }
                }
                /**
                 * 调用相应的方法apply()或者commit()
                 * 之后,notifyListeners()通知监听器
                 */
                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;
    }

监听器的实现比较监听,用了一个WeakHashMap存放所有的监听器,WeahHashMap在更新的时候会将空的内容移除。

至此,这个MultiProcessSharedPreferences算是码完了
源码地址:有道笔记SharedPreference

总结

其实整个思想就是自己项目内部的人员,当这个类如平时的SharedPreference使用,而且可以实现跨进程,中间小小的转变就是继承ContentProvider,然后数据源还是SharedPreference。整个感觉就是
- 内部使用:SharedPreference -> ContentProvider -> SharedPreference数据源
- 外部使用:ContentProvider -> SharedPreference数据源

一句话总结:
==用ContentProvider做了一下中间媒介,实现了跨进程访问.==