SharePreference原理及跨進程數(shù)據(jù)共享的問題

SharedPreferences是Android提供的數(shù)據(jù)持久化的一種手段,適合單進程、小批量的數(shù)據(jù)存儲與訪問。為什么這么說呢?因為SharedPreferences的實現(xiàn)是基于單個xml文件實現(xiàn)的,并且,所有持久化數(shù)據(jù)都是一次性加載到內(nèi)存,如果數(shù)據(jù)過大,是不合適采用SharedPreferences存放的。而適用的場景是單進程的原因同樣如此,由于Android原生的文件訪問并不支持多進程互斥,所以SharePreferences也不支持,如果多個進程更新同一個xml文件,就可能存在同不互斥問題,后面會詳細分析這幾個問題。

SharedPreferences的實現(xiàn)原理之:持久化數(shù)據(jù)的加載

首先,從基本使用簡單看下SharedPreferences的實現(xiàn)原理:

    mSharedPreferences = context.getSharedPreferences("test", Context.MODE_PRIVATE);
    SharedPreferences.Editor editor = mSharedPreferences.edit();
    editor.putString(key, value);
    editor.apply();

context.getSharedPreferences其實就是簡單的調(diào)用ContextImpl的getSharedPreferences,具體實現(xiàn)如下

       @Override
public SharedPreferences getSharedPreferences(String name, int mode) {
    SharedPreferencesImpl sp;
    synchronized (ContextImpl.class) {
        if (sSharedPrefs == null) {
            sSharedPrefs = new ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>>();
        }

        final String packageName = getPackageName();
        ArrayMap<String, SharedPreferencesImpl> packagePrefs = sSharedPrefs.get(packageName);
        if (packagePrefs == null) {
            packagePrefs = new ArrayMap<String, SharedPreferencesImpl>();
            sSharedPrefs.put(packageName, packagePrefs);
        }
        sp = packagePrefs.get(name);
        if (sp == null) {
        <!--讀取文件-->
            File prefsFile = getSharedPrefsFile(name);
            sp = new SharedPreferencesImpl(prefsFile, mode);
            <!--緩存sp對象-->
            packagePrefs.put(name, sp);
            return sp;
        }
    }
    <!--跨進程同步問題-->
    if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
        getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
        sp.startReloadIfChangedUnexpectedly();
    }
    return sp;
}

以上代碼非常簡單,直接描述下來就是先去內(nèi)存中查詢與xml對應的SharePreferences是否已經(jīng)被創(chuàng)建加載,如果沒有那么該創(chuàng)建就創(chuàng)建,該加載就加載,在加載之后,要將所有的key-value保存到內(nèi)幕才能中去,當然,如果首次訪問,可能連xml文件都不存在,那么還需要創(chuàng)建xml文件,與SharePreferences對應的xml文件位置一般都在/data/data/包名/shared_prefs目錄下,后綴一定是.xml,數(shù)據(jù)存儲樣式如下

sp對應的xml數(shù)據(jù)存儲模型

這里面數(shù)據(jù)的加載的地方需要看下,比如,SharePreferences數(shù)據(jù)的加載是同步還是異步?數(shù)據(jù)加載是new SharedPreferencesImpl對象時候開始的,

 SharedPreferencesImpl(File file, int mode) {
    mFile = file;
    mBackupFile = makeBackupFile(file);
    mMode = mode;
    mLoaded = false;
    mMap = null;
    startLoadFromDisk();
}

startLoadFromDisk很簡單,就是讀取xml配置,如果其他線程想要在讀取之前就是用的話,就會被阻塞,一直wait等待,直到數(shù)據(jù)讀取完成。

    private void loadFromDiskLocked() {
   ...
    Map map = null;
    StructStat stat = null;
    try {
        stat = Os.stat(mFile.getPath());
        if (mFile.canRead()) {
            BufferedInputStream str = null;
            try {
    <!--讀取xml中配置-->
                str = new BufferedInputStream(
                        new FileInputStream(mFile), 16*1024);
                map = XmlUtils.readMapXml(str);
            }...
    mLoaded = true;
    ...
    <!--喚起其他等待線程-->
    notifyAll();
}

可以看到其實就是直接使用xml解析工具XmlUtils,直接在當前線程讀取xml文件,所以,如果xml文件稍大,盡量不要在主線程讀取,讀取完成之后,xml中的配置項都會被加載到內(nèi)存,再次訪問的時候,其實訪問的是內(nèi)存緩存。

SharedPreferences的實現(xiàn)原理之:持久化數(shù)據(jù)的更新

通常更新SharedPreferences的時候是首先獲取一個SharedPreferences.Editor,利用它緩存一批操作,之后當做事務提交,有點類似于數(shù)據(jù)庫的批量更新:

    SharedPreferences.Editor editor = mSharedPreferences.edit();
    editor.putString(key1, value1);
    editor.putString(key2, value2);
    editor.putString(key3, value3);
    editor.apply();//或者commit

Editor是一個接口,這里的實現(xiàn)是一個EditorImpl對象,它首先批量預處理更新操作,之后再提交更新,在提交事務的時候有兩種方式,一種是apply,另一種commit,兩者的區(qū)別在于:何時將數(shù)據(jù)持久化到xml文件,前者是異步的,后者是同步的。Google推薦使用前一種,因為,就單進程而言,只要保證內(nèi)存緩存正確就能保證運行時數(shù)據(jù)的正確性,而持久化,不必太及時,這種手段在Android中使用還是很常見的,比如權限的更新也是這樣,況且,Google并不希望SharePreferences用于多進程,因為不安全,手下卡一下apply與commit的區(qū)別

    public void apply() {
    <!--添加到內(nèi)存-->
        final MemoryCommitResult mcr = commitToMemory();
        final Runnable awaitCommit = new Runnable() {
                public void run() {
                    try {
                        mcr.writtenToDiskLatch.await();
                    } catch (InterruptedException ignored) {
                    }
                }
            };

        QueuedWork.add(awaitCommit);
        Runnable postWriteRunnable = new Runnable() {
                public void run() {
                    awaitCommit.run();
                    QueuedWork.remove(awaitCommit);
                }
            };
        <!--延遲寫入到xml文件-->
        SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
        <!--通知數(shù)據(jù)變化-->
        notifyListeners(mcr);
    }
 
 public boolean commit() {
        MemoryCommitResult mcr = commitToMemory();
        SharedPreferencesImpl.this.enqueueDiskWrite(
            mcr, null /* sync write on this thread okay */);
        try {
            mcr.writtenToDiskLatch.await();
        } catch (InterruptedException e) {
            return false;
        }
        notifyListeners(mcr);
        return mcr.writeToDiskResult;
    }     

從上面可以看出兩者最后都是先調(diào)用commitToMemory,將更改提交到內(nèi)存,在這一點上兩者是一致的,之后又都調(diào)用了enqueueDiskWrite進行數(shù)據(jù)持久化任務,不過commit函數(shù)一般會在當前線程直接寫文件,而apply則提交一個事務到已給線程池,之后直接返回,實現(xiàn)如下:

 private void enqueueDiskWrite(final MemoryCommitResult mcr,
                              final Runnable postWriteRunnable) {
    final Runnable writeToDiskRunnable = new Runnable() {
            public void run() {
                synchronized (mWritingToDiskLock) {
                    writeToFile(mcr);
                }
                synchronized (SharedPreferencesImpl.this) {
                    mDiskWritesInFlight--;
                }
                if (postWriteRunnable != null) {
                    postWriteRunnable.run();
                }
            }
        };
   final boolean isFromSyncCommit = (postWriteRunnable == null);
    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (SharedPreferencesImpl.this) {
            wasEmpty = mDiskWritesInFlight == 1;
        }
        <!--如果沒有其他線程在寫文件,直接在當前線程執(zhí)行-->
        if (wasEmpty) {
            writeToDiskRunnable.run();
            return;
        }
    }
   QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}

不過如果有線程在寫文件,那么就不能直接寫,這個時候就跟apply函數(shù)一致了,但是,如果直觀說兩者的區(qū)別的話,直接說commit同步,而apply異步應該也是沒有多大問題的

SharePreferences多進程使用問題

SharePreferences在新建的有個mode參數(shù),可以指定它的加載模式,MODE_MULTI_PROCESS是Google提供的一個多進程模式,但是這種模式并不是我們說的支持多進程同步更新等,它的作用只會在getSharedPreferences的時候,才會重新從xml重加載,如果我們在一個進程中更新xml,但是沒有通知另一個進程,那么另一個進程的SharePreferences是不會自動更新的。

@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
    SharedPreferencesImpl 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;
}

也就是說MODE_MULTI_PROCESS只是個雞肋Flag,對于多進程的支持幾乎為0,下面是Google文檔,簡而言之,就是:不要用

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。

響應的Google為多進程提供了一個數(shù)據(jù)同步互斥方案,那就是基于Binder實現(xiàn)的ContentProvider,關于ContentProvider后文分析。

總結(jié)

  • SharePreferences是Android基于xml實現(xiàn)的一種數(shù)據(jù)持久話手段
  • SharePreferences不支持多進程
  • SharePreferences的commit與apply一個是同步一個是異步(大部分場景下)
  • 不要使用SharePreferences存儲太大的數(shù)據(jù)

作者:看書的小蝸牛
原文鏈接:SharePreference原理及跨進程數(shù)據(jù)共享的問題
僅供參考,歡迎指正

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容