SharePreference原理及跨進程數據共享的問題

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

SharedPreferences的實現原理之:持久化數據的加載

首先,從基本使用簡單看下SharedPreferences的實現原理:

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

context.getSharedPreferences其實就是簡單的調用ContextImpl的getSharedPreferences,具體實現如下

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

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

sp對應的xml數據存儲模型

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

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

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

    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中的配置項都會被加載到內存,再次訪問的時候,其實訪問的是內存緩存。

SharedPreferences的實現原理之:持久化數據的更新

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

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

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

    public void apply() {
    <!--添加到內存-->
        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);
        <!--通知數據變化-->
        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;
    }     

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

 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;
        }
        <!--如果沒有其他線程在寫文件,直接在當前線程執行-->
        if (wasEmpty) {
            writeToDiskRunnable.run();
            return;
        }
    }
   QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}

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

SharePreferences多進程使用問題

SharePreferences在新建的有個mode參數,可以指定它的加載模式,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爲多進程提供了一個數據同步互斥方案,那就是基於Binder實現的ContentProvider,關於ContentProvider後文分析。

總結

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

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

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