流行框架原始碼分析(6)-多程式的sharedprefrence解決方案DPreference

weixin_34413065發表於2017-10-16

主目錄見:Android高階進階知識(這是總目錄索引)
 我們都知道sharedpreference在使用的時候是不支援多程式運算元據的,不同程式間運算元據的讀取,存取或者併發運算元據都會出現問題,所以我們需要自己去控制跨程式操作,現在我們看看官方文件對shareprefrence中MODE_MULTI_PROCESS的描述:

int MODE_MULTI_PROCESS
This constant was deprecated in API level 23.
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 ContentProvider.

這段英文的意思就是這個常量在api 23就已經廢棄了,MODE_MULTI_PROCESS在一些android版本中不能穩定執行,而且不提供任何機制來記錄跨程式的修改,不建議應用使用他。作為替代,應該使用更加明確的跨程式資料管理辦法比如ContentProvider。所以這裡我們必須尋求一個更好的解決這個問題的辦法,我們今天這裡選擇ContentProvider+sharedpreference的辦法即框架[DPreference],當然還有框架ContentProvider+sqLite的[Tray],但是看驗證效能不理想,而且需要升級相關問題,所以我們這裡選擇DPreference來講解,好啦,看了這麼多放鬆一下。。。

7709098-0c34cddc3e9075a5.png
清醒一下

一.目標

 今天這篇文章也是我在用跨程式框架時候遇到需要儲存資料,然後多個程式操作的時候遇到的問題,其實有很多種辦法解決,但是這種方法我覺得是比較方便的。學習今天的文章有幾個目標:
1.複習ContentProvider的使用,因為這個平常用的確實沒有很多;
2.可以多一個多程式存取資料的解決方案。

二.原始碼分析

首先還是跟其他原始碼的入手點一樣,我們先來看看最基本的使用方法,其實這個使用方法跟shareprefrence是一模一樣的:

 DPreference dPreference = new DPreference(context, "default");
dPreference.setPrefString( "key", "value");

 DPreference dPreference = new DPreference(context, "default");
dPreference.getPrefString( "key");

我們看到確實用法跟shareprefrence無異,甚至更簡單有沒有,不用commit。現在我們先從儲存開始講解。

1.儲存 setPrefString

首先我們從DPreference的建構函式開始看:

  public DPreference(Context context, String name) {
        this.mContext = context;
        this.mName = name;
    }

我們看到建構函式啥也沒有,只是簡單地賦值一下,那麼我們來看此類的setPrefString方法吧:

    public void setPrefString(final String key, final String value) {
        PrefAccessor.setString(mContext, mName, key, value);
    }

這個方法裡面又呼叫了PrefAccessor類的setString方法,我們跟進去看下:

   public static void setString(Context context, String name, String key, String value) {
        Uri URI = PreferenceProvider.buildUri(name, key, PreferenceProvider.PREF_STRING);
        ContentValues cv = new ContentValues();
        cv.put(PreferenceProvider.PREF_KEY, key);
        cv.put(PreferenceProvider.PREF_VALUE, value);
        context.getContentResolver().update(URI, cv, null, null);
    }

我們看到這裡面是典型的訪問ContentProvider的方法,這裡我們看下PreferenceProvider中的buildUri方法。PreferenceProvider是個ContentProvider物件,我們首先看下Uri是什麼:

  private static final String AUTHORITY = "me.dozen.dpreference.PreferenceProvider";

    public static final String CONTENT_PREF_BOOLEAN_URI = "content://" + AUTHORITY + "/boolean/";
    public static final String CONTENT_PREF_STRING_URI = "content://" + AUTHORITY + "/string/";
    public static final String CONTENT_PREF_INT_URI = "content://" + AUTHORITY + "/integer/";
    public static final String CONTENT_PREF_LONG_URI = "content://" + AUTHORITY + "/long/";


    public static final String PREF_KEY = "key";
    public static final String PREF_VALUE = "value";

    public static final int PREF_BOOLEAN = 1;
    public static final int PREF_STRING = 2;
    public static final int PREF_INT = 3;
    public static final int PREF_LONG = 4;

    private static final UriMatcher sUriMatcher;

    static {
        sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        sUriMatcher.addURI(AUTHORITY, "boolean/*/*", PREF_BOOLEAN);
        sUriMatcher.addURI(AUTHORITY, "string/*/*", PREF_STRING);
        sUriMatcher.addURI(AUTHORITY, "integer/*/*", PREF_INT);
        sUriMatcher.addURI(AUTHORITY, "long/*/*", PREF_LONG);

    }

我們看到這個就是我們ContentProvider典型的做法,暴露幾個Uri給外部訪問,具體的細節相信大家知道了,如果不知道我這裡推薦一篇文章[Android開發之內容提供者——建立自己的ContentProvider(詳解) ],相信看了大家就會明白的。然後我們來看buildUri方法:

  public static Uri buildUri(String name, String key, int type) {
        return Uri.parse(getUriByType(type) + name + "/" + key);
    }

    private static String getUriByType(int type) {
        switch (type) {
            case PreferenceProvider.PREF_BOOLEAN:
                return PreferenceProvider.CONTENT_PREF_BOOLEAN_URI;
            case PreferenceProvider.PREF_INT:
                return PreferenceProvider.CONTENT_PREF_INT_URI;
            case PreferenceProvider.PREF_LONG:
                return PreferenceProvider.CONTENT_PREF_LONG_URI;
            case PreferenceProvider.PREF_STRING:
                return PreferenceProvider.CONTENT_PREF_STRING_URI;
        }
        throw new IllegalStateException("unsupport preftype : " + type);
    }

我們看到這裡程式碼會根據type來獲取對應的Uri,到這裡我們已經獲取到了Uri,我們就可以用ContentResolver呼叫ContentProvider裡面相應的方法了,我們看到我們這裡的setString()方法最後呼叫了ContentProvider的update()方法,所以我們進一步就是看這個PreferenceProvider的update()方法:

 @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        PrefModel model = getPrefModelByUri(uri);
        if(model == null) {
            throw new IllegalArgumentException("update prefModel is null");
        }
        switch (sUriMatcher.match(uri)) {
            case PREF_BOOLEAN:
                persistBoolean(model.getName(), values);
                break;
            case PREF_LONG:
                persistLong(model.getName(), values);
                break;
            case PREF_STRING:
                persistString(model.getName(), values);
                break;
            case PREF_INT:
                persistInt(model.getName(), values);
                break;
            default:
                throw new IllegalStateException("update unsupported uri : " + uri);
        }
        return 0;
    }

這個方法其實就是要儲存我們設定進來的value,我們先看這個方法最開始會查詢PrefModel的物件,所以我們看下這個getPrefModelByUri()方法:

 private PrefModel getPrefModelByUri(Uri uri) {
        if (uri == null || uri.getPathSegments().size() != 3) {
            throw new IllegalArgumentException("getPrefModelByUri uri is wrong : " + uri);
        }
        String name = uri.getPathSegments().get(1);
        String key = uri.getPathSegments().get(2);
        return new PrefModel(name, key);
    }

其中getPathSegments方法的getPathSegments得到uri的path部分,並拆分,去掉"/",取到第一個元素(從第0個開始),
//比如:
content://"+FirstProvierMetaData.AUTHORIY+"/users /1"
//getPathSegments()得到的是users 和1,get(1)會得到1

所以這裡的get(1)得到的name(就是對應於DPreference(context, "default")這裡面的default,也就是這個DPreference的名字),get(2)就是獲取到key就是setString()方法裡面的key,然後會匹配Uri呼叫相應方法,我們字串會匹配到persistString()方法:

    private void persistString(String name, ContentValues values) {
        if (values == null) {
            throw new IllegalArgumentException(" values is null!!!");
        }
        String kString = values.getAsString(PREF_KEY);
        String vString = values.getAsString(PREF_VALUE);
        getDPreference(name).setPrefString(kString, vString);
    }

我們這裡看到會根據name來獲取到一個對應的shareprefrence,具體我們看getDPreference方法:

    private IPrefImpl getDPreference(String name) {
        if (TextUtils.isEmpty(name)) {
            throw new IllegalArgumentException("getDPreference name is null!!!");
        }
        if (sPreferences.get(name) == null) {
            IPrefImpl pref = new PreferenceImpl(getContext(), name);
            sPreferences.put(name, pref);
        }
        return sPreferences.get(name);
    }

我們看到這裡會先在sPreferences(private static Map<String, IPrefImpl> sPreferences = new ArrayMap<>())中獲取到這個name對應的PreferenceImpl物件,如果不存在則儲存,然後返回PreferenceImpl物件,接著就是呼叫setPrefString()方法,那麼這個方法就是PreferenceImpl的setPrefString方法:

  public void setPrefString(final String key, final String value) {
        final SharedPreferences settings =
                mContext.getSharedPreferences(mPrefName, Context.MODE_PRIVATE);
        settings.edit().putString(key, value).apply();
    }

我們看到方法最終還是呼叫了sharedpreference的方法,非常的簡單,到這裡我們的儲存方法已經完畢了,是不是很簡單,其實的確是很簡單。

2.獲取 getPrefString

上面已經講完怎麼存了,就是利用ContentProvider機制,但是最終還是用shareprefrence進行儲存,那麼我們這裡取肯定最終也是從shareprefrence中獲取的嘛,我們開始驗證:

  public String getPrefString(final String key, final String defaultValue) {
        return PrefAccessor.getString(mContext, mName, key, defaultValue);
    }

這裡套路是一樣的,也是呼叫的PrefAccessor裡面的getString方法:

 public static String getString(Context context, String name, String key, String defaultValue) {
        Uri URI = PreferenceProvider.buildUri(name, key, PreferenceProvider.PREF_STRING);
        String value = defaultValue;
        Cursor cursor = context.getContentResolver().query(URI, null, null, null, null);
        if (cursor != null && cursor.moveToFirst()) {
            value = cursor.getString(cursor.getColumnIndex(PreferenceProvider.PREF_VALUE));
        }
        IOUtils.closeQuietly(cursor);
        return value;
    }

我們看到第一句也是獲取Uri,這個跟前面講過的是一樣的,這裡就不贅述,然後我們看利用ContentResolver呼叫了query方法,我們就看ContentProvider的query方法到底做了啥:

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        MatrixCursor cursor = null;
        PrefModel model = getPrefModelByUri(uri);
        switch (sUriMatcher.match(uri)) {
            case PREF_BOOLEAN:
                if (getDPreference(model.getName()).hasKey(model.getKey())) {
                    cursor = preferenceToCursor(getDPreference(model.getName()).getPrefBoolean(model.getKey(), false) ? 1 : 0);
                }
                break;
            case PREF_STRING:
                if (getDPreference(model.getName()).hasKey(model.getKey())) {
                    cursor = preferenceToCursor(getDPreference(model.getName()).getPrefString(model.getKey(), ""));
                }
                break;
            case PREF_INT:
                if (getDPreference(model.getName()).hasKey(model.getKey())) {
                    cursor = preferenceToCursor(getDPreference(model.getName()).getPrefInt(model.getKey(), -1));
                }
                break;
            case PREF_LONG:
                if (getDPreference(model.getName()).hasKey(model.getKey())) {
                    cursor = preferenceToCursor(getDPreference(model.getName()).getPrefLong(model.getKey(), -1));
                }
                break;
        }
        return cursor;
    }

首先看上面這段程式碼之前,我們需要知道一個概念,那就是MatrixCursor類的作用,如果想得到一個Cursor, 而此時又沒有資料庫返回一個Cursor,此時可以通過MatrixCursor來返回一個Cursor,因為我們這裡底層是用sharedpreference,不是用的sqlite,所以我們要返回搜尋的結果Cursor,我們就只能用這個類了。我們程式匹配的時候還是會呼叫到preferenceToCursor(當然在呼叫之前是會判斷key在不在的這個name對應的sharedpreference中),我們首先看到preferenceToCursor裡面的引數就是getDPreference(model.getName()).getPrefString(model.getKey(), "")獲取得到的,我們首先看下PreferenceImpl的getPrefString方法:

  public String getPrefString(final String key,
                                final String defaultValue) {
        final SharedPreferences settings =
                mContext.getSharedPreferences(mPrefName, Context.MODE_PRIVATE);
        return settings.getString(key, defaultValue);
    }

我們看到這裡呼叫了sharedpreference來獲取值,然後傳給preferenceToCursor方法:

private <T> MatrixCursor preferenceToCursor(T value) {
        MatrixCursor matrixCursor = new MatrixCursor(PREFERENCE_COLUMNS, 1);
        MatrixCursor.RowBuilder builder = matrixCursor.newRow();
        builder.add(value);
        return matrixCursor;
    }

然後我們看到這個value被新增進MatrixCursor 的物件中,然後返回這個Cursor,就可以像運算元據庫返回的Cursor那樣了。到這裡我們的資料也就取完畢了。整個流程是比較容易的,就是ContentProvider的基本知識。
總結:這篇文章主要就是利用ContentProvider來包裝SharedPereference來操作key和value的值,整體來說是比較簡單,但是這是一個解決問題的辦法,還是很不錯的,希望大家有get到技能。

相關文章