SharedPreferences 多進程問題

SharedPreferences作為一種數(shù)據(jù)持久化的方式,是處理簡單的key-value類型數(shù)據(jù)時的首選。但是有時候需要在多進程中共享數(shù)據(jù)時,如果用SharedPreferences會不會有什么問題?即SharedPreferences是否是進程安全的?

SharedPreferences如何實現(xiàn)多進程訪問?

談論進程安全前,先看下SharedPreferences如何實現(xiàn)多進程。我們知道每個SharedPreferences都對應了當前package的data/data/package_name/share_prefs/目錄下的一個文件,要讓多個進程可訪問這個文件,必然需要修改其訪問權(quán)限,看下SharedPreferences提供了哪些選項, 在Context.java中提供了以下幾個mode:

int MODE_PRIVATE = 0x0000;
這是默認模式,僅caller uid的進程可訪問

int MODE_WORLD_READABLE = 0x0001; 
所有人可寫,也就是任何應用都可修改它,這是極其危險的,因此改選項已被Deprected

int MODE_WORLD_READABLE = 0x0002
所有人可讀,這個參數(shù)同樣非常危險,可能導致隱私數(shù)據(jù)泄漏

int MODE_MULTI_PROCESS = 0x0004;
設置該參數(shù)后,每次獲取對應的SharedPreferences時都會嘗試從磁盤中讀取修改過的文件 

多進程訪問主要有兩種情況:
1.不同apk(不同packageName,且不具有相同uid)的多進程:
由于linux文件權(quán)限是根據(jù)uid設置訪問權(quán)限,因此必須設置mode為MODE_WORLD_READABLE或MODE_WORLD_WRITABLE,取決于別的應用是否有需要需改該SharedPreferences,由于這種方式需要修改文件權(quán)限,且會讓所有人都具訪問權(quán)限,無法只對某個應用授權(quán),所以非常危險,android N上對targetsdk >= 24的應用已明確禁止這兩個mode,本文不再做過多解釋
2.同一個packageName或具有相同uid的package里面存在多個進程:
這種情況下多個進程具有相同的uid,因此不需要修改文件權(quán)限,沒有權(quán)限安全性問題。目前很多apk都支持多進程,例如后臺服務與前臺頁面運行在獨立的進程。這種情況是本文重點分析的。

是否需要設置MODE_MULTI_PROCESS?

先看下這個mode具體描述

/**
     * 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.
     *
     * <p>This was the legacy (but undocumented) behavior in and
     * before Gingerbread (Android 2.3) and this flag is implied when
     * targetting such releases.  For applications targetting SDK
     * versions <em>greater than</em> Android 2.3, this flag must be
     * explicitly set if desired.
     *
     * @see #getSharedPreferences
     *
     * @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}.
     */

當設置這個參數(shù)的時候,即使當前進程內(nèi)已經(jīng)創(chuàng)建了該SharedPreferences,仍然在每次獲取的時候都會嘗試從本地文件中刷新。在同一個進程中,同一個文件只有一個實例。MODE_MULTI_PROCESS的作用
ContextImpl.java

public SharedPreferences  getSharedPreferences(String name, int mode) {
    //code: new instance if not exists
    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;
}

這個方法先判斷是否已創(chuàng)建SharedPreferences實例,若未創(chuàng)建,則先創(chuàng)建。之后判斷mode如果為MODE_MULTI_PROCESS, 則調(diào)用startReloadIfChangeUnexpectedly(),看下其實現(xiàn):
SharedPreferencesImpl.java

void startReloadIfChangedUnexpectedly() {
        synchronized (this) {
            // TODO: wait for any pending writes to disk?
            if (!hasFileChangedUnexpectedly()) {
                return;
            }
            startLoadFromDisk();
        }
    }

最終調(diào)用startLoadFromDisk()

private void startLoadFromDisk() {
        synchronized (this) {
            mLoaded = false;
        }
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                synchronized (SharedPreferencesImpl.this) {
                    loadFromDiskLocked();
                }
            }
        }.start();
    }

可以看出MODE_MULTI_PROCESS的作用就是在每次獲取SharedPreferences實例的時候嘗試從磁盤中加載修改過的數(shù)據(jù),并且讀取是在異步線程中,因此一個線程的修改最終會反映到另一個線程,但不能立即反映到另一個進程,所以通過SharedPreferences無法實現(xiàn)多進程同步。
綜合: 如果僅僅讓多進程可訪問同一個SharedPref文件,不需要設置MODE_MULTI_PROCESS, 如果需要實現(xiàn)多進程同步,必須設置這個參數(shù),但也只能實現(xiàn)最終一致,無法即時同步。

為什么不推薦使用MODE_MULTI_PROCESS?

android文檔已經(jīng)Deprected了這個flag,并且說明不應該通過SharedPreference做進程間數(shù)據(jù)共享?這是為啥呢?從前面但分析可看到當設置這個flag后,每次獲取(獲取而不是初次創(chuàng)建)SharedPreferences實例的時候,會判斷shared_pref文件是否修改過:

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

這里先判斷mDiskWritesInFlight>0,如果成立,說明是當前進程修改了文件,不需要重新讀取。然后通過文件最后修改時間,判斷文件是否修改過。如果修改了,則重新讀取:

private void startLoadFromDisk() {
        synchronized (this) {
            mLoaded = false;
        }
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                synchronized (SharedPreferencesImpl.this) {
                    loadFromDiskLocked();
                }
            }
        }.start();
}

private void loadFromDiskLocked() {
        if (mLoaded) {
            return;
        }
        if (mBackupFile.exists()) {
            mFile.delete();
            mBackupFile.renameTo(mFile);
        }
        Map map = null;
        StructStat stat = null;
        try {
            stat = Os.stat(mFile.getPath());
            if (mFile.canRead()) {
                BufferedInputStream str = null;
                try {
                    str = new BufferedInputStream(
                            new FileInputStream(mFile), 16*1024);
                    map = XmlUtils.readMapXml(str);
                } finally {
                    IoUtils.closeQuietly(str);
                }
            }
        } catch (ErrnoException e) {
        }
        mLoaded = true;
        if (map != null) {
            mMap = map;
            mStatTimestamp = stat.st_mtime;
            mStatSize = stat.st_size;
        } else {
            mMap = new HashMap<String, Object>();
        }
        notifyAll();
}

重點是這段:

if (mBackupFile.exists()) {
      mFile.delete();
      mBackupFile.renameTo(mFile);
}

重新讀取時,如果發(fā)現(xiàn)存在mBackupFile,則將原文件mFile刪除,并將mBackupFile重命名為mFile。mBackupFile又是如何創(chuàng)建的呢?答案是在修改SharedPreferences時將內(nèi)存中的數(shù)據(jù)寫會磁盤時創(chuàng)建的:

private void writeToFile(MemoryCommitResult mcr) {
        // Rename the current file so it may be used as a backup during the next read
        if (mFile.exists()) {
            if (!mBackupFile.exists()) {
                if (!mFile.renameTo(mBackupFile)) {
                    mcr.setDiskWriteResult(false);
                    return;
                }
            } else {
                mFile.delete();
            }
        }
        FileOutputStream str = createFileOutputStream(mFile);
        XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
        FileUtils.sync(str);
        str.close();
        ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
        final StructStat stat = Os.stat(mFile.getPath());
        synchronized (this) {
            mStatTimestamp = stat.st_mtime;
            mStatSize = stat.st_size;
        }
        // Writing was successful, delete the backup file if there is one.
        mBackupFile.delete();
        mcr.setDiskWriteResult(true);
        return;
    }

這段代碼只保留了核心流程,忽略了錯誤處理流程。可以看到,寫文件的步驟大致是:

  1. 將原文件重命名為mBackupFile
  2. 重新創(chuàng)建原文件mFile, 并將內(nèi)容寫入其中
  3. 刪除mBackupFile
    所以,只有當一個進程正處于寫文件的過程中的時候,如果另一個進程讀文件,才會看到mBackupFile, 這時候讀進程會將mBackupFile重命名為mFile, 這樣讀結(jié)果是,讀進程只能都到修改前的文件,同時,由于mBackupFile重命名為了mFile, 所以寫進程寫那個文件就沒有文件名引用了,因此其寫入的內(nèi)容無法再被任何進程訪問到。所以其內(nèi)容丟失了,可認為寫入失敗了,而SharedPreferences對這種失敗情況沒有任何重試機制,所以就可能出現(xiàn)數(shù)據(jù)丟失的情況。
    回到這段的重點:為什么不推薦用MODE_MULTI_PROCESS?從前面分析可知,這種模式下,每次獲取SharedPreferences都會檢測文件是否改變,只要讀的時候另一進程在寫,就會導致寫丟失。這樣失敗概率就會大幅度提高。反之,若不設置這個模式,則只在第一次創(chuàng)建SharedPreferences的時候讀取,導致寫失敗的概率就會大幅度降低,當然,仍然存在失敗的可能。

為什么不做寫失敗重試?

為毛android不做寫失敗重試呢?原因是寫進程并不能發(fā)現(xiàn)寫失敗的情況。難道寫的過程中,目標文件被刪不會拋異常嗎?答案是不會。刪除文件只是從文件系統(tǒng)中刪除了一個節(jié)點信息而已,重命名也是新建了一個具有相同名稱的節(jié)點信息,并把文件地址指向另一個磁盤地址而已,原來,之前的寫過程仍然會成功寫到原來的磁盤地址。所以目前的實現(xiàn)方案并不能檢測到失敗。

有沒有辦法解決寫失敗呢?

個人覺得是可以做到的,讀里面讀那段關(guān)鍵操作:

if (mBackupFile.exists()) {
      mFile.delete();
      mBackupFile.renameTo(mFile);
}

mBackupFile存在,意味著當前正處于寫讀過程中,這時候是不是可以考慮直接讀mBackupFile文件,而不刪除mFile呢?這樣讀話,讀取效果一樣,都是讀的mBackupFile,同時寫進程寫的mFile也不會被mBacupFile覆蓋,寫也就能成功了。即使通過這段代碼重命名,寫進程寫完后發(fā)現(xiàn)mBackupFile不存在了,其實也能認為發(fā)生了讀重命名,大可以重試一次。

讀文件過程中,文件被刪除會導致讀失敗嗎?

不會!與重命名一樣,文件被刪除只是刪掉節(jié)點信息,磁盤上的文件仍然存在,知道所有打開文件的fd都釋放,文件才會真正被刪除。

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

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