Android SharedPreference 支持多進程

Android SharedPreference 支持多進程

96 
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肯定要快一些,優先推薦使用apply


SharedPreferenceImpl是如何創建的呢,在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個坑!

  1. 使用MODE_MULTI_PROCESS時,不要保存SharedPreference變量,必須每次都從context.getSharedPreferences 獲取。如果你圖方便使用變量存了下來,那麼無法觸發reload,有可能兩個進程數據不同步。
  2. 前面提到過,load數據是耗時的,並且其他操作會等待該鎖。這意味着很多時候獲取SharedPreference數據都不得不從文件再讀一遍,大大降低了內存緩存的作用。文件讀寫耗時也影響了性能。
  3. 修改數據時得用commit,保證修改時寫入了文件,這樣其他進程才能通過文件大小或修改時間感知到。

綜上,無論怎麼說,MODE_MULTI_PROCESS都很糟糕,避免使用就對了。


多進程使用SharedPreference方案

說簡單也簡單,就是依據google的建議使用ContentProvider了。我看過網上很多的例子,但總是覺得少了點什麼

  1. 有的方案裏將所有讀取操作都寫作靜態方法,沒有繼承SharedPreference 。 這樣做需要強制改變調用者的使用習慣,不怎麼好。
  2. 大部分方案做成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操作封裝的類。
所有跨進程的操作都是通過SharedPreferenceProvidercall方法完成。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>
注意兩點:

  1. 獲取SharedPreferences使用的都是MODE_PRIVATE模式,其他的模式比較少見,基本沒怎麼用。
  2. 在跨進程的SharedPreferenceProxy 裏,registerOnSharedPreferenceChangeListener暫時還沒有實現,可以使用ContentObserver實現跨進程監聽。

詳細代碼見:https://github.com/liyuanhust/MultiprocessPreference

轉自:https://www.jianshu.com/p/875d13458538

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章