細數 SharedPreferences 的那些槽點 !

来源:https://www.cnblogs.com/bingxinshuo/archive/2019/08/28/11427208.html
-Advertisement-
Play Games

前言 最近在處理一個歷史遺留項目的時候飽受其害,主要表現為偶發性的 SharedPreferences 配置文件數據錯亂,甚至丟失。經過排查發現是多進程的問題。項目中有兩個不同進程,且會頻繁的讀寫 SharedPreferences 文件,所以導致了數據錯亂和丟失。趁此機會,精讀了一遍 Shared ...


前言

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

源碼解析

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

1. 獲取 SharedPreferences

1.1 getDefaultSharedPreferences()

一般我們會通過 PreferenceManagergetDefaultSharedPreferences() 方法來獲取預設的 SharedPreferences 對象,其代碼如下所示:

> 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() 方法。

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,找一下它的定義:

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

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

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 就可以了。

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 題解。

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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • [TOC] 1.文件夾(庫) 增: 改: 查: 刪: 2.文件(表) 增: 改: 查: 刪: 3.文件的一行內容 增: 改: 查: 刪: 4.創建表的完整語法 5.整型類型 | 類型 | 大小 | 範圍(有符號) | 範圍(無符號)unsigned約束 | 用途 | | | | | | | | TI ...
  • 概念 LRU(Least Recently Used)最近最少使用演算法是眾多置換演算法中的一種。 maxmemory Redis中有一個maxmemory概念,主要是為了將使用的記憶體限定在一個固定的大小。Redis用到的LRU 演算法,是一種近似的LRU演算法。 設置maxmemory 註意,在64bit ...
  • flink是一款開源的大數據流式處理框架,他可以同時批處理和流處理,具有容錯性、高吞吐、低延遲等優勢,本文簡述flink在windows和linux中安裝步驟,和示常式序的運行。 首先要想運行Flink,我們需要下載並解壓Flink的二進位包,下載地址如下:https://flink.apache. ...
  • 四、簡單查詢 ​ 簡單查詢的主要特征就是將一張數據表之中的全部數據行進行顯示,而後可以利用 SELECT 子句來控制所需要的輸出列。 4.1、基礎語法 範例 :查詢 emp 表中的數據(全部數據查詢) ​ 在取得全部數據後,可以發現某些列上會顯示 null 的信息,null 表示的是沒有內容,但 n ...
  • 在資料庫view的創建中,會遇到一些跨資料庫的view腳本,但是在將view更新到production的時候可能忘記更改database name,導致出現一些問題。 以下腳本可以檢查出包含某個關鍵字的view name,只需要修改 條件即可 TSql type(v,fn,p) select nam ...
  • [學習筆記] 3)配置環境變數:(環境變數中的~1,~2,~3的用法)i)JAVA_HOME:註意C:\Program Files目錄存在空格,變成C:\Progra~1\Java\jdk1.8.0_144。(註意:長於8個字元的文件名和文件夾名,都被簡化成前面6個有效字元,後面~1,有重名的就 ~ ...
  • 1. 簡介 資料庫,現代化的數據存儲存儲手段,是一種特殊的文件,其中存儲著需要的數據。 特點: 持久化存儲 讀寫速度極高 保證數據的有效性 對程式支持性非常好,容易擴展 2. Mysql (1)具有數據完整性: 一個資料庫就是一個完整的業務單元,可以包含多張表,數據被存儲在表中。在表中為了更加準確的 ...
  • 設置主機名 [root@localhost ~]# cat /etc/hosts127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4::1 localhost localhost.localdomai... ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...