細數 SharedPreferences 的那些槽點 !

前言

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

源碼解析

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

1. 獲取 SharedPreferences

1.1 getDefaultSharedPreferences()

一般我們會通過 PreferenceManager 的 getDefaultSharedPreferences() 方法來獲取默認的 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,其實現在也只用這種模式了,後面的源碼解析中也會提到。最後都會調用到 ContextImpl 的 getSharedPreferences() 方法。

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_READABLE 和 MODE_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() 對象。Editor 和 SharedPreferences 一樣,都只是接口,它們的實現類分別是 EditorImpl 和 SharedPreferencesImpl

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

有兩個成員變量,mModified 和 mClearmModified 是一個 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);

第二個參數 postWriteRunnable 是 null,所以 isFromSyncCommit 爲 true,會執行上面的 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.java 中 handlePauseActivity() 方法爲例:

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

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

發佈了78 篇原創文章 · 獲贊 6 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章