前言 最近在處理一個歷史遺留項目的時候飽受其害,主要表現為偶發性的 SharedPreferences 配置文件數據錯亂,甚至丟失。經過排查發現是多進程的問題。項目中有兩個不同進程,且會頻繁的讀寫 SharedPreferences 文件,所以導致了數據錯亂和丟失。趁此機會,精讀了一遍 Shared ...
前言
最近在處理一個歷史遺留項目的時候飽受其害,主要表現為偶發性的 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.xml
。mode
預設為 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(); // 喚醒處於等待狀態的線程
}
}
}
簡單捋一下流程:
- 判斷是否已經載入進記憶體
- 判斷是否存在遺留的備份文件,如果存在,重命名為 sp 文件
- 讀取 sp 文件,並存入記憶體
- 更新文件信息
- 釋放鎖,喚醒處於等待狀態的線程
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
和 mClear
。mModified
是一個 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 源碼,槽點可真不少!
- 不支持跨進程,
MODE_MULTI_PROCESS
也沒用。跨進程頻繁讀寫可能導致數據損壞或丟失。 - 初始化的時候會讀取 sp 文件,可能導致後續
getXXX()
方法阻塞。建議提前非同步初始化 SharedPreferences。 - sp 文件的數據會全部保存在記憶體中,所以不宜存放大數據。
edit()
方法每次都會新建一個EditorImpl
對象。建議一次 edit(),多次 putXXX() 。- 無論是
commit()
還是apply()
,針對任何修改都是全量寫入。建議針對高頻修改的配置項存在子啊單獨的 sp 文件。 commit()
同步保存,有返回值。apply()
非同步保存,無返回值。按需取用。onPause()
、onReceive()
等時機會等待非同步寫操作執行完成,可能造成卡頓或者 ANR。
這麼多問題,我們是不是不應該使用 SharedPreferences 呢?答案肯定不是的。如果你不需要跨進程,僅僅存儲少量的配置項,SharedPreferences 仍然是一個很好的選擇。
如果 SharedPreferences 已經滿足不了你的需求了,給你推薦 Tencent 開源的 MMKV !
文章首發微信公眾號:
秉心說
, 專註 Java 、 Android 原創知識分享,LeetCode 題解。更多最新原創文章,掃碼關註我吧!