細數 SharedPreferences 的那些槽點 !

前言

最近在處理一個歷史遺留項目的時候飽受其害,主要表現爲偶發性的 SharedPreferences 配置文件數據錯亂,甚至丟失。通過排查發現是多進程的問題。項目中有兩個不一樣進程,且會頻繁的讀寫 SharedPreferences 文件,因此致使了數據錯亂和丟失。趁此機會,精讀了一遍 SharedPreferences 源碼,下面就來講說 SharedPreferences 都有哪些槽點。java

源碼解析

SharedPreferences 的使用很簡單,這裏就再也不演示了。下面就按 獲取 SharedPreferencegetXXX() 獲取數據putXXX()存儲數據 這三方面來閱讀源碼。android

1. 獲取 SharedPreferences

1.1 getDefaultSharedPreferences()

通常咱們會經過 PreferenceManagergetDefaultSharedPreferences() 方法來獲取默認的 SharedPreferences 對象,其代碼以下所示:c++

> PreferenceManager.java 

/**
 * 獲取默認的 SharedPreferences 對象,文件名爲 packageName_preferences , mode 爲 MODE_PRIVATE
 */
public static SharedPreferences getDefaultSharedPreferences(Context context) {
    return context.getSharedPreferences(getDefaultSharedPreferencesName(context),
            getDefaultSharedPreferencesMode());  // 見 1.2
}

默認的 sp 文件完整路徑爲 /data/data/shared_prefs/[packageName]_preferences.xmlmode 默認爲 MODE_PRIVATE,其實如今也只用這種模式了,後面的源碼解析中也會提到。最後都會調用到 ContextImplgetSharedPreferences() 方法。git

1.2 getSharedPreferences(String name, int mode)

> ContextImpl.java

@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
    // At least one application in the world actually passes in a null
    // name.  This happened to work because when we generated the file name
    // we would stringify it to "null.xml".  Nice.
    if (mPackageInfo.getApplicationInfo().targetSdkVersion <
            Build.VERSION_CODES.KITKAT) {
        if (name == null) {
            name = "null";
        }
    }

    File file;
    synchronized (ContextImpl.class) {
        if (mSharedPrefsPaths == null) {
            mSharedPrefsPaths = new ArrayMap<>();
        }
        // 先從緩存 mSharedPrefsPaths 中查找 sp 文件是否存在
        file = mSharedPrefsPaths.get(name);
        if (file == null) { // 若是不存在,新建 sp 文件,文件名爲 "name.xml"
            file = getSharedPreferencesPath(name);
            mSharedPrefsPaths.put(name, file);
        }
    }
    return getSharedPreferences(file, mode); // 見 1.3
}

首先這裏出現了一個變量 mSharedPrefsPaths,找一下它的定義:github

/**
 * 文件名爲 key,具體文件爲 value。存儲全部 sp 文件
 * 由 ContextImpl.class 鎖保護
 */
@GuardedBy("ContextImpl.class")
private ArrayMap<String, File> mSharedPrefsPaths;

mSharedPrefsPaths 是一個 ArrayMap ,緩存了文件名和 sp 文件的對應關係。首先會根據參數中的文件名 name 查找緩存中是否存在對應的 sp 文件。若是不存在的話,會新建名稱爲 [name].xml 的文件,並存入緩存 mSharedPrefsPaths 中。最後會調用另外一個重載的 getSharedPreferences() 方法,參數是 File 。segmentfault

1.3 getSharedPreferences(File file, int mode)

> ContextImpl.java

@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
    SharedPreferencesImpl sp;
    synchronized (ContextImpl.class) {
        final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked(); // 見 1.3.1
        sp = cache.get(file); // 先從緩存中嘗試獲取 sp
        if (sp == null) { // 若是獲取緩存失敗
            checkMode(mode); // 檢查 mode,見 1.3.2
            if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) {
                if (isCredentialProtectedStorage()
                        && !getSystemService(UserManager.class)
                                .isUserUnlockingOrUnlocked(UserHandle.myUserId())) {
                    throw new IllegalStateException("SharedPreferences in credential encrypted "
                            + "storage are not available until after user is unlocked");
                }
            }
            sp = new SharedPreferencesImpl(file, mode); // 建立 SharedPreferencesImpl,見 1.4
            cache.put(file, sp);
            return sp;
        }
    }

    // mode 爲 MODE_MULTI_PROCESS 時,文件可能被其餘進程修改,則從新加載
    // 顯然這並不足以保證跨進程安全
    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;
}

SharedPreferences 只是接口而已,咱們要獲取的其實是它的實現類 SharedPreferencesImpl 。經過 getSharedPreferencesCacheLocked() 方法能夠獲取已經緩存的 SharedPreferencesImpl 對象和其 sp 文件。緩存

1.3.1 getSharedPreferencesCacheLocked()
> ContextImpl.java

private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
    if (sSharedPrefsCache == null) {
        sSharedPrefsCache = new ArrayMap<>();
    }

    final String packageName = getPackageName();
    ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
    if (packagePrefs == null) {
        packagePrefs = new ArrayMap<>();
        sSharedPrefsCache.put(packageName, packagePrefs);
    }

    return packagePrefs;
}

sSharedPrefsCache 是一個嵌套的 ArrayMap,其定義以下:安全

private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;

以包名爲 key ,以一個存儲了 sp 文件及其 SharedPreferencesImp 對象的 ArrayMap 爲 value。若是存在直接返回,反之建立一個新的 ArrayMap 做爲值並存入緩存。微信

1.3.2 checkMode()
> ContextImpl.java

private void checkMode(int mode) {
    // 從 N 開始,若是使用 MODE_WORLD_READABLE 和 MODE_WORLD_WRITEABLE,直接拋出異常
    if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.N) {
        if ((mode & MODE_WORLD_READABLE) != 0) {
            throw new SecurityException("MODE_WORLD_READABLE no longer supported");
        }
        if ((mode & MODE_WORLD_WRITEABLE) != 0) {
            throw new SecurityException("MODE_WORLD_WRITEABLE no longer supported");
        }
    }
}

從 Android N 開始,明確再也不支持 MODE_WORLD_READABLEMODE_WORLD_WRITEABLE,再加上 MODE_MULTI_PROCESS 並不能保證線程安全,通常就使用 MODE_PRIVATE 就能夠了。app

1.4 SharedPreferencesImpl

若是緩存中沒有對應的 SharedPreferencesImpl 對象,就得本身建立了。看一下它的構造函數:

SharedPreferencesImpl(File file, int mode) {
    mFile = file; // sp 文件
    mBackupFile = makeBackupFile(file); // 建立備份文件
    mMode = mode; 
    mLoaded = false; // 標識 sp 文件是否已經加載到內存
    mMap = null; // 存儲 sp 文件中的鍵值對
    mThrowable = null;
    startLoadFromDisk(); // 加載數據,見 1.4.1
}

注意這裏的 mMap,它是一個 Map<String, Object>,存儲了 sp 文件中的全部鍵值對。因此 SharedPreferences 文件的全部數據都是存在於內存中的,既然存在於內存中,就註定它不適合存儲大量數據。

1.4.1 startLoadFromDisk()
> SharedPreferencesImpl.java

private void startLoadFromDisk() {
    synchronized (mLock) {
        mLoaded = false;
    }
    new Thread("SharedPreferencesImpl-load") {
        public void run() {
            loadFromDisk(); // 異步加載。 見 1.4.2
        }
    }.start();
}
1.4.2 loadFromDisk()
> SharedPreferencesImpl.java

private void loadFromDisk() {
    synchronized (mLock) { // 獲取 mLock 鎖
        if (mLoaded) { // 已經加載進內存,直接返回,再也不讀取文件
            return;
        }
        if (mBackupFile.exists()) { // 若是存在備份文件,直接將備份文件重命名爲 sp 文件
            mFile.delete();
            mBackupFile.renameTo(mFile);
        }
    }

    // Debugging
    if (mFile.exists() && !mFile.canRead()) {
        Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");
    }

    Map<String, Object> map = null;
    StructStat stat = null;
    Throwable thrown = null;
    try { // 讀取 sp 文件
        stat = Os.stat(mFile.getPath());
        if (mFile.canRead()) {
            BufferedInputStream str = null;
            try {
                str = new BufferedInputStream(
                        new FileInputStream(mFile), 16 * 1024);
                map = (Map<String, Object>) XmlUtils.readMapXml(str);
            } catch (Exception e) {
                Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
            } finally {
                IoUtils.closeQuietly(str);
            }
        }
    } catch (ErrnoException e) {
        // An errno exception means the stat failed. Treat as empty/non-existing by
        // ignoring.
    } catch (Throwable t) {
        thrown = t;
    }

    synchronized (mLock) {
        mLoaded = true;
        mThrowable = thrown;

        // It's important that we always signal waiters, even if we'll make
        // them fail with an exception. The try-finally is pretty wide, but
        // better safe than sorry.
        try {
            if (thrown == null) {
                if (map != null) {
                    mMap = map;
                    mStatTimestamp = stat.st_mtim; // 更新修改時間
                    mStatSize = stat.st_size; // 更新文件大小
                } else {
                    mMap = new HashMap<>();
                }
            }
            // In case of a thrown exception, we retain the old map. That allows
            // any open editors to commit and store updates.
        } catch (Throwable t) {
            mThrowable = t;
        } finally {
            mLock.notifyAll(); // 喚醒處於等待狀態的線程
        }
    }
}

簡單捋一下流程:

  1. 判斷是否已經加載進內存
  2. 判斷是否存在遺留的備份文件,若是存在,重命名爲 sp 文件
  3. 讀取 sp 文件,並存入內存
  4. 更新文件信息
  5. 釋放鎖,喚醒處於等待狀態的線程

loadFromDisk() 是異步執行的,並且是線程安全的,讀取過程當中持有鎖 mLock,看起來設計的都很合理,可是在不合理的使用狀況下就會出現問題。

看了這麼長的源碼,別忘了咱們還停留在 getSharedPreferences() 方法,也就是獲取 SharedPreferences 的過程當中。若是咱們在使用過程當中,調用 getSharedPreferences() 以後,直接調用 getXXX() 方法來獲取數據,剛好 sp 文件數據量又比較大,讀取過程比較耗時,getXXX() 方法就會被阻塞。後面看到 getXXX() 方法的源碼時,你就會看到它須要等待 sp 文件加載完成,不然就會阻塞。因此在使用過程當中,能夠提早異步初始化 SharedPreferences 對象,加載 sp 文件進內存,避免發生潛在可能的卡頓。這是 SharedPreferences 的一個槽點,也是咱們使用過程當中須要注意的。

2. 讀取 sp 數據

獲取 sp 文件中的數據使用的是 SharedPreferencesImpl 中的七個 getXXX 函數。這七個函數都是同樣的邏輯,以 getInt() 爲例看一下源碼:

> SharedPreferencesImpl.java

@Override
public int getInt(String key, int defValue) {
    synchronized (mLock) {
        awaitLoadedLocked(); // sp 文件還沒有加載完成時,會阻塞在這裏,見 2.1
        Integer v = (Integer)mMap.get(key); // 加載完成後直接從內存中讀取
        return v != null ? v : defValue;
    }
}

一旦 sp 文件加載完成,全部獲取數據的操做都是從內存中讀取的。這樣的確提高了效率,可是很顯然將大量的數據直接放在內存是不合適的,因此註定了 SharedPreferences 不適合存儲大量數據。

2.1 awaitLoadedLocked()

> SharedPreferencesImpl.java

@GuardedBy("mLock")
private void awaitLoadedLocked() {
    if (!mLoaded) {
        // Raise an explicit StrictMode onReadFromDisk for this
        // thread, since the real read will be in a different
        // thread and otherwise ignored by StrictMode.
        BlockGuard.getThreadPolicy().onReadFromDisk();
    }
    while (!mLoaded) { // sp 文件還沒有加載完成時, 等待
        try {
            mLock.wait();
        } catch (InterruptedException unused) {
        }
    }
    if (mThrowable != null) {
        throw new IllegalStateException(mThrowable);
    }
}

mLoaded 初始值爲 false,在 loadFromDisk() 方法中讀取 sp 文件以後會被置爲 true,並調用 mLock.notifyAll() 通知等待的線程。

3. 存儲 sp 數據

SharedPreferences 存儲數據的基本方法以下:

val editor = PreferenceManager.getDefaultSharedPreferences(this).edit()
editor.putInt("key",1)
editor.commit()/editor.apply()

edit() 方法會返回一個 Editor() 對象。EditorSharedPreferences 同樣,都只是接口,它們的實現類分別是 EditorImplSharedPreferencesImpl

3.1 edit()

> SharedPreferencesImpl.java

@Override
public Editor edit() {
    synchronized (mLock) {
        awaitLoadedLocked(); // 等待 sp 文件加載完成
    }

    return new EditorImpl(); // 見 3.2
}

edit() 方法一樣也要等待 sp 文件加載完成,再進行 EditImpl() 的初始化。每次調用 edit() 方法都會實例化一個新的 EditorImpl 對象。因此咱們在使用的時候要注意不要每次 put() 都去調用 edit() 方法,在封裝 SharedPreferences 工具類的時候可能會犯這個錯誤。

3.2 EditorImpl

> SharedPreferencesImpl.java

public final class EditorImpl implements Editor {
    private final Object mEditorLock = new Object();

    @GuardedBy("mEditorLock")
    private final Map<String, Object> mModified = new HashMap<>(); // 存儲要修改的數據

    @GuardedBy("mEditorLock")
    private boolean mClear = false; // 清除標記

    @Override
    public Editor putString(String key, @Nullable String value) {
        synchronized (mEditorLock) {
            mModified.put(key, value);
            return this;
        }
    }
    
    @Override
    public Editor remove(String key) {
        synchronized (mEditorLock) {
            mModified.put(key, this);
            return this;
        }
    }

    @Override
    public Editor clear() {
        synchronized (mEditorLock) {
            mClear = true;
            return this;
        }
    }
    
    @Override
    public boolean commit() { } // 見 3.2.1
    
    @Override
    public boolean apply() { } // 見 3.2.2

有兩個成員變量,mModifiedmClearmModified 是一個 HashMap,存儲了全部經過 putXXX() 方法添加的須要添加或者修改的鍵值對。mClear 是清除標記,在 clear() 方法中會被置爲 true

全部的 putXXX() 方法都只是改變了 mModified 集合,當調用 commit() 或者 apply() 時纔會去修改 sp 文件。下面分別看一下這兩個方法。

3.2.1 commit()
> SharedPreferencesImpl.java

@Override
    public boolean commit() {
        long startTime = 0;

        if (DEBUG) {
            startTime = System.currentTimeMillis();
        }

        // 先將 mModified 同步到內存
        MemoryCommitResult mcr = commitToMemory(); // 見 3.2.2

        // 再將內存數據同步到文件,見 3.2.3
        SharedPreferencesImpl.this.enqueueDiskWrite(
            mcr, null /* sync write on this thread okay */);
        try {
            mcr.writtenToDiskLatch.await(); // 等待寫入操做完成
        } catch (InterruptedException e) {
            return false;
        } finally {
            if (DEBUG) {
                Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                        + " committed after " + (System.currentTimeMillis() - startTime)
                        + " ms");
            }
        }
        notifyListeners(mcr); // 通知監聽者,回調 OnSharedPreferenceChangeListener
        return mcr.writeToDiskResult; // 返回寫入操做結果
    }

commit() 的大體流程是:

  • 首先同步 mModified 到內存中 , commitToMemory()
  • 而後同步內存數據到 sp 文件中 ,enqueueDiskWrite()
  • 等待寫入操做完成,並通知監聽者

內存同步是 commitToMemory() 方法,寫入文件是 enqueueDiskWrite() 方法。來詳細看一下這兩個方法。

3.2.2 commitToMemory()
> SharedPreferencesImpl.java

// Returns true if any changes were made
private MemoryCommitResult commitToMemory() {
    long memoryStateGeneration;
    List<String> keysModified = null;
    Set<OnSharedPreferenceChangeListener> listeners = null;
    Map<String, Object> mapToWriteToDisk;

    synchronized (SharedPreferencesImpl.this.mLock) {
        // 在 commit() 的寫入本地文件過程當中,會將 mDiskWritesInFlight 置爲 1.
        // 寫入過程還沒有完成時,又調用了 commitToMemory(),直接修改 mMap 可能會影響寫入結果
        // 因此這裏要對 mMap 進行一次深拷貝
        if (mDiskWritesInFlight > 0) {   
            mMap = new HashMap<String, Object>(mMap);
        }
        mapToWriteToDisk = mMap;
        mDiskWritesInFlight++;

        boolean hasListeners = mListeners.size() > 0;
        if (hasListeners) {
            keysModified = new ArrayList<String>();
            listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
        }

        synchronized (mEditorLock) {
            boolean changesMade = false;

            if (mClear) {
                if (!mapToWriteToDisk.isEmpty()) {
                    changesMade = true;
                    mapToWriteToDisk.clear();
                }
                mClear = false;
            }

            for (Map.Entry<String, Object> e : mModified.entrySet()) {
                String k = e.getKey();
                Object v = e.getValue();
                // "this" is the magic value for a removal mutation. In addition,
                // setting a value to "null" for a given key is specified to be
                // equivalent to calling remove on that key.
                // v == this 和 v == null 都表示刪除此 key
                if (v == this || v == null) {
                    if (!mapToWriteToDisk.containsKey(k)) {
                        continue;
                    }
                    mapToWriteToDisk.remove(k);
                } else {
                    if (mapToWriteToDisk.containsKey(k)) {
                        Object existingValue = mapToWriteToDisk.get(k);
                        if (existingValue != null && existingValue.equals(v)) {
                            continue;
                        }
                    }
                    mapToWriteToDisk.put(k, v);
                }

                changesMade = true;
                if (hasListeners) {
                    keysModified.add(k);
                }
            }

            mModified.clear();

            if (changesMade) {
                mCurrentMemoryStateGeneration++;
            }

            memoryStateGeneration = mCurrentMemoryStateGeneration;
        }
    }
    return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
            mapToWriteToDisk);
}

簡單說,commitToMemory() 方法會將全部須要改動的數據 mModified 和原 sp 文件數據 mMap 進行合併生成一個新的數據集合 mapToWriteToDisk,從名字也能夠看出來,這就是以後要寫入文件的數據集。沒錯,SharedPreferences 的寫入都是全量寫入。即便你只改動了其中一個配置項,也會從新寫入全部數據。針對這一點,咱們能夠作的優化是,將須要頻繁改動的配置項使用單獨的 sp 文件進行存儲,避免每次都要全量寫入。

3.2.3 enqueueDiskWrite()

> SharedPreferencesImpl.java

private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                final Runnable postWriteRunnable) {
    final boolean isFromSyncCommit = (postWriteRunnable == null);

    final Runnable writeToDiskRunnable = new Runnable() {
        @Override
        public void run() {
            synchronized (mWritingToDiskLock) {
                writeToFile(mcr, isFromSyncCommit); // 見 3.2.3.1
            }
            synchronized (mLock) {
                mDiskWritesInFlight--;
            }
            if (postWriteRunnable != null) {
                postWriteRunnable.run();
            }
        }
    };

    // Typical #commit() path with fewer allocations, doing a write on
    // the current thread.
    // commit() 直接在當前線程進行寫入操做
    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (mLock) {
            wasEmpty = mDiskWritesInFlight == 1;
        }
        if (wasEmpty) {
            writeToDiskRunnable.run();
            return;
        }
    }

    // apply() 方法執行此處,由 QueuedWork.QueuedWorkHandler 處理
    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

回頭先看一下 commit() 方法中是如何調用 enqueueDiskWrite() 方法的:

SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null);

第二個參數 postWriteRunnablenull,因此 isFromSyncCommittrue,會執行上面的 if 代碼塊,而不執行 QueuedWork.queue()。因而可知,commit() 方法最後的寫文件操做是直接在當前調用線程執行的,你在主線程調用該方法,就會直接在主線程進行 IO 操做。顯然,這是不建議的,可能形成卡頓或者 ANR。在實際使用中咱們應該儘可能使用 apply() 方法來提交數據。固然,apply() 也並非十全十美的,後面咱們會提到。

3.2.3.1 writeToFile()

commit() 方法的最後一步了,將 mapToWriteToDisk 寫入 sp 文件。

> SharedPreferencesImpl.java

private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
        long startTime = 0;
        long existsTime = 0;
        long backupExistsTime = 0;
        long outputStreamCreateTime = 0;
        long writeTime = 0;
        long fsyncTime = 0;
        long setPermTime = 0;
        long fstatTime = 0;
        long deleteTime = 0;

        if (DEBUG) {
            startTime = System.currentTimeMillis();
        }

        boolean fileExists = mFile.exists();

        if (DEBUG) {
            existsTime = System.currentTimeMillis();

            // Might not be set, hence init them to a default value
            backupExistsTime = existsTime;
        }

        // Rename the current file so it may be used as a backup during the next read
        if (fileExists) {
            boolean needsWrite = false;

            // Only need to write if the disk state is older than this commit
            // 僅當磁盤狀態比當前提交舊時草須要寫入文件
            if (mDiskStateGeneration < mcr.memoryStateGeneration) {
                if (isFromSyncCommit) {
                    needsWrite = true;
                } else {
                    synchronized (mLock) {
                        // No need to persist intermediate states. Just wait for the latest state to
                        // be persisted.
                        if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
                            needsWrite = true;
                        }
                    }
                }
            }

            if (!needsWrite) { // 無需寫入,直接返回
                mcr.setDiskWriteResult(false, true);
                return;
            }

            boolean backupFileExists = mBackupFile.exists(); // 備份文件是否存在

            if (DEBUG) {
                backupExistsTime = System.currentTimeMillis();
            }

            // 若是備份文件不存在,將 mFile 重命名爲備份文件,供之後遇到異常時使用
            if (!backupFileExists) {
                if (!mFile.renameTo(mBackupFile)) {
                    Log.e(TAG, "Couldn't rename file " + mFile
                          + " to backup file " + mBackupFile);
                    mcr.setDiskWriteResult(false, false);
                    return;
                }
            } else {
                mFile.delete();
            }
        }

        // Attempt to write the file, delete the backup and return true as atomically as
        // possible.  If any exception occurs, delete the new file; next time we will restore
        // from the backup.
        try {
            FileOutputStream str = createFileOutputStream(mFile);

            if (DEBUG) {
                outputStreamCreateTime = System.currentTimeMillis();
            }

            if (str == null) {
                mcr.setDiskWriteResult(false, false);
                return;
            }
            XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str); // 全量寫入

            writeTime = System.currentTimeMillis();

            FileUtils.sync(str);

            fsyncTime = System.currentTimeMillis();

            str.close();
            ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);

            if (DEBUG) {
                setPermTime = System.currentTimeMillis();
            }

            try {
                final StructStat stat = Os.stat(mFile.getPath());
                synchronized (mLock) {
                    mStatTimestamp = stat.st_mtim; // 更新文件時間
                    mStatSize = stat.st_size; // 更新文件大小
                }
            } catch (ErrnoException e) {
                // Do nothing
            }

            if (DEBUG) {
                fstatTime = System.currentTimeMillis();
            }

            // Writing was successful, delete the backup file if there is one.
            // 寫入成功,刪除備份文件
            mBackupFile.delete();

            if (DEBUG) {
                deleteTime = System.currentTimeMillis();
            }

            mDiskStateGeneration = mcr.memoryStateGeneration;

            // 返回寫入成功,喚醒等待線程
            mcr.setDiskWriteResult(true, true);

            if (DEBUG) {
                Log.d(TAG, "write: " + (existsTime - startTime) + "/"
                        + (backupExistsTime - startTime) + "/"
                        + (outputStreamCreateTime - startTime) + "/"
                        + (writeTime - startTime) + "/"
                        + (fsyncTime - startTime) + "/"
                        + (setPermTime - startTime) + "/"
                        + (fstatTime - startTime) + "/"
                        + (deleteTime - startTime));
            }

            long fsyncDuration = fsyncTime - writeTime;
            mSyncTimes.add((int) fsyncDuration);
            mNumSync++;

            if (DEBUG || mNumSync % 1024 == 0 || fsyncDuration > MAX_FSYNC_DURATION_MILLIS) {
                mSyncTimes.log(TAG, "Time required to fsync " + mFile + ": ");
            }

            return;
        } catch (XmlPullParserException e) {
            Log.w(TAG, "writeToFile: Got exception:", e);
        } catch (IOException e) {
            Log.w(TAG, "writeToFile: Got exception:", e);
        }

        // Clean up an unsuccessfully written file
        // 清除未成功寫入的文件
        if (mFile.exists()) {
            if (!mFile.delete()) {
                Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
            }
        }
        mcr.setDiskWriteResult(false, false); // 返回寫入失敗
    }

流程比較清晰,代碼也比較簡單,

3.2.4 apply()
> SharedPreferencesImpl.java

@Override
public void apply() {
    final long startTime = System.currentTimeMillis();

    // 先將 mModified 同步到內存
    final MemoryCommitResult mcr = commitToMemory();
    final Runnable awaitCommit = new Runnable() {
            @Override
            public void run() {
                try {
                    mcr.writtenToDiskLatch.await();
                } catch (InterruptedException ignored) {
                }

                if (DEBUG && mcr.wasWritten) {
                    Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                            + " applied after " + (System.currentTimeMillis() - startTime)
                            + " ms");
                }
            }
        };

    QueuedWork.addFinisher(awaitCommit);

    Runnable postWriteRunnable = new Runnable() {
            @Override
            public void run() {
                awaitCommit.run();
                QueuedWork.removeFinisher(awaitCommit);
            }
        };

    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

    // Okay to notify the listeners before it's hit disk
    // because the listeners should always get the same
    // SharedPreferences instance back, which has the
    // changes reflected in memory.
    notifyListeners(mcr);
}

一樣也是先調用 commitToMemory() 同步到內存,再調用 enqueueDiskWrite() 同步到文件。和 commit() 不一樣的是,enqueueDiskWrite() 方法的 Runnable 參數再也不是 null 了,傳進來一個 postWriteRunnable 。因此其內部的執行邏輯和 commit() 方法是徹底不一樣的。能夠再回到 3.2.3 節看一下,commit() 方法會直接在當前線程執行 writeToDiskRunnable(),而 apply() 會由 QueuedWork 來處理:

QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit); // 見 3.2.5
3.2.5 queue()
> QueuedWork.java

public static void queue(Runnable work, boolean shouldDelay) {
    Handler handler = getHandler();

    synchronized (sLock) {
        sWork.add(work);

        if (shouldDelay && sCanDelay) {
            handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
        } else {
            handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
        }
    }
}

這裏的 handler 所在的線程就是執行 Runnable 的線程了,看一下 getHandler 源碼:

> QueuedWork.java

private static Handler getHandler() {
    synchronized (sLock) {
        if (sHandler == null) {
            HandlerThread handlerThread = new HandlerThread("queued-work-looper",
                    Process.THREAD_PRIORITY_FOREGROUND);
            handlerThread.start();

            sHandler = new QueuedWorkHandler(handlerThread.getLooper());
        }
        return sHandler;
    }
}

寫 sp 文件的操做會異步執行在一個單獨的線程上。

QueuedWork 除了執行異步操做以外,還有一個做用。它能夠確保當 Activity onPause()/onStop() 以後,或者 BroadCast onReceive() 以後,異步任務能夠執行完成。以 ActivityThread.javahandlePauseActivity() 方法爲例:

@Override
public void handleStopActivity(IBinder token, boolean show, int configChanges,
        PendingTransactionActions pendingActions, boolean finalStateRequest, String reason) {
    final ActivityClientRecord r = mActivities.get(token);
    r.activity.mConfigChangeFlags |= configChanges;

    final StopInfo stopInfo = new StopInfo();
    performStopActivityInner(r, stopInfo, show, true /* saveState */, finalStateRequest,
            reason);

    if (localLOGV) Slog.v(
        TAG, "Finishing stop of " + r + ": show=" + show
        + " win=" + r.window);

    updateVisibility(r, show);

    // Make sure any pending writes are now committed.
    // 可能因等待寫入形成卡頓甚至 ANR
    if (!r.isPreHoneycomb()) {
        QueuedWork.waitToFinish();
    }

    stopInfo.setActivity(r);
    stopInfo.setState(r.state);
    stopInfo.setPersistentState(r.persistentState);
    pendingActions.setStopInfo(stopInfo);
    mSomeActivitiesChanged = true;
}

初衷多是好的,可是咱們都知道在 Activity() 的 onPause()/onStop() 中不該該進行耗時任務。若是 sp 數據量很大的話,這裏無疑會出現性能問題,可能形成卡頓甚至 ANR。

總結

擼完 SharedPreferences 源碼,槽點可真很多!

  1. 不支持跨進程,MODE_MULTI_PROCESS 也沒用。跨進程頻繁讀寫可能致使數據損壞或丟失。
  2. 初始化的時候會讀取 sp 文件,可能致使後續 getXXX() 方法阻塞。建議提早異步初始化 SharedPreferences。
  3. sp 文件的數據會所有保存在內存中,因此不宜存放大數據。
  4. edit() 方法每次都會新建一個 EditorImpl 對象。建議一次 edit(),屢次 putXXX() 。
  5. 不管是 commit() 仍是 apply() ,針對任何修改都是全量寫入。建議針對高頻修改的配置項存在子啊單獨的 sp 文件。
  6. commit() 同步保存,有返回值。apply() 異步保存,無返回值。按需取用。
  7. onPause()onReceive() 等時機會等待異步寫操做執行完成,可能形成卡頓或者 ANR。

這麼多問題,咱們是否是不該該使用 SharedPreferences 呢?答案確定不是的。若是你不須要跨進程,僅僅存儲少許的配置項,SharedPreferences 仍然是一個很好的選擇。

若是 SharedPreferences 已經知足不了你的需求了,給你推薦 Tencent 開源的 MMKV !

文章首發微信公衆號: 秉心說 , 專一 Java 、 Android 原創知識分享,LeetCode 題解。

更多最新原創文章,掃碼關注我吧!

相關文章
相關標籤/搜索