SharePreference原理及跨程式資料共享的問題

看書的小蝸牛發表於2017-11-15

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資料儲存模型
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原理及跨程式資料共享的問題
僅供參考,歡迎指正

相關文章