SharedPreferences anr 原因以及避免方案
技術(shù)背景:
AuthMode 和SDK 使用了系統(tǒng)默認(rèn)的 SharedPreferences,系統(tǒng)的 SharedPreferences 實(shí)現(xiàn)類在 android/app/SharedPrefenencesImpl.java 中。
然后出現(xiàn)了類似這樣的 ANR:
Cmd line: com.android.settings
at java.lang.Object.wait(Native Method)
- waiting on <0x41897ec8> (a java.lang.VMThread) held by tid=1 (main)
at java.lang.Thread.parkFor(Thread.java:1205)
at sun.misc.Unsafe.park(Unsafe.java:325)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:157)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:813)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireSharedInterruptibly(AbstractQueuedSynchronizer.java:973)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireSharedInterruptibly(AbstractQueuedSynchronizer.java:1281)
at java.util.concurrent.CountDownLatch.await(CountDownLatch.java:202)
at android.app.SharedPreferencesImpl$EditorImpl$1.run(SharedPreferencesImpl.java:364)
at android.app.QueuedWork.waitToFinish(QueuedWork.java:88)
at android.app.ActivityThread.handleStopActivity(ActivityThread.java:3418)
at android.app.ActivityThread.access$1100(ActivityThread.java:154)
SharedPrefenences 工作流程
我們平時(shí)這樣獲取一個(gè) SharedPrefenences(全文使用 SP 作為SharedPrefenences的簡稱)
context.getSharedPreferences("", Context.MODE_PRIVATE)
然后极祸,無論是 Activity 和 Application 對應(yīng)的 Context翅帜,其實(shí)現(xiàn)鏈路如下:
//直接調(diào)用的是 ContextWrapper 里的方法 在 ContextWarpper.java 中
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
return mBase.getSharedPreferences(name, mode);
}
// mBase 對象是 ContextImpl 的實(shí)例,在 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<>();
}
file = mSharedPrefsPaths.get(name);
if (file == null) {
file = getSharedPreferencesPath(name);
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode);
}
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
//首先會(huì)從 Cache 中去拿,見方法
sp = cache.get(file);
if (sp == null) {
checkMode(mode);
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);
cache.put(file, sp);
return sp;
}
}
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;
}
@GuardedBy("ContextImpl.class")
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
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;
}
那么獲取一個(gè) SharedPreferences 的流程為:
-
根據(jù)傳入的 SharedPreferences 名稱,去讀取對應(yīng)的文件,如果文件不存在則創(chuàng)建文件芝此,默認(rèn)的路徑在 data/data/your package name/shared_prefs 文件夾下,也就是改代碼創(chuàng)建的路徑因痛。
new File(getDataDir(), "shared_prefs");
獲取文件之后婚苹,調(diào)用 getSharedPreferencesCacheLocked() 獲取在 static ArrayMap 該應(yīng)用包名緩存的一個(gè) SharedPreferences ArrayMap。然后查看是否有該文件對應(yīng)的緩存 SharedPerferences鸵膏。
-
如果沒有緩存的 SP膊升,則會(huì)創(chuàng)建一個(gè),調(diào)用代碼如下:
sp = new SharedPreferencesImpl(file, mode);
將sp 緩存到第二步獲得的 ArrayMap谭企,這是一個(gè) static 的變量廓译。
那么接下來講下,SP IO 操作的相關(guān)流程:
在上面的方法中赞咙,實(shí)際創(chuàng)建 SharedPreferences 的是這一行代碼:
sp = new SharedPreferencesImpl(file, mode);
那么對應(yīng)的構(gòu)造方法如下:
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
startLoadFromDisk();
}
實(shí)際上先會(huì)這里開啟了子線程進(jìn)行了IO操作:
//SharedPreferencesImpl.java
private void startLoadFromDisk() {
synchronized (mLock) {//對mLock 加鎖
mLoaded = false;//注意這個(gè)變量
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
IO 操作的代碼也很簡單责循,可以簡單看一下:
private void loadFromDisk() {
synchronized (mLock) {
if (mLoaded) {
return;
}
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
}
// Debugging
if (mFile.exists() && !mFile.canRead()) {
Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");
}
Map map = null;
StructStat stat = null;
try {
stat = Os.stat(mFile.getPath());
if (mFile.canRead()) {
BufferedInputStream str = null;
try {
str = new BufferedInputStream(
new FileInputStream(mFile), 16*1024);
map = XmlUtils.readMapXml(str);
} catch (Exception e) {
Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
} finally {
IoUtils.closeQuietly(str);
}
}
} catch (ErrnoException e) {
/* ignore */
}
synchronized (mLock) {
mLoaded = true;//這里 mLoaded為 true 了
if (map != null) {
mMap = map;
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
mLock.notifyAll();//這里會(huì)喚醒所有的等待鎖
}
}
所以在主線程調(diào)用 getSharedPreferences() 糟港,會(huì)開啟子線程去IO 操作File攀操,這是沒問題的,但是如果你調(diào)用 SP 的 getXXX() 方法的時(shí)候秸抚,就可能有問題了速和,且看下面的分析。
讀取 SharedPreferences 導(dǎo)致 ANR 的根本原因
例如你從sp 中讀取一個(gè) String剥汤,會(huì)調(diào)用到 getString() 方法:
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
private void awaitLoadedLocked() {
if (!mLoaded) {//sp 對象創(chuàng)建完成颠放,mLoaded 才會(huì)是 true
BlockGuard.getThreadPolicy().onReadFromDisk();
}
while (!mLoaded) {
try {
mLock.wait();
} catch (InterruptedException unused) {
}
}
}
那么這里會(huì)調(diào)用 awaitLoadedLocked() 直到該 SP 對象創(chuàng)建完成,所以這里就對導(dǎo)致主線程等待吭敢。從上面知道碰凶,只有 SP 對應(yīng)的xml 解析完了,并且創(chuàng)建出 SP 對象鹿驼,mLoaded 才會(huì)是 true欲低,否則就會(huì)一直等待。如果你存儲(chǔ)的 SP 特別大畜晰,那么可能就會(huì)導(dǎo)致主線程 ANR砾莱。
SharedPreferences 性能代價(jià)
從上面的分析知道,我們的 SP 讀取過一次之后凄鼻,就會(huì)在一個(gè) static 的 ArrayMap 中存儲(chǔ)起來腊瑟,如下:
//ContextImpl.java 中
@GuardedBy("ContextImpl.class")
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
那么實(shí)際上要避免這種操作聚假,避免存儲(chǔ)超大的字符串。
SharedPreferences commit() 和 apply() 原理
我們往 sp 寫入內(nèi)容闰非,一般如下:
sp.edit().putString("","").commit()
其中 edit() 方法獲取的是一個(gè)Editor膘格,其實(shí)現(xiàn)類是 EditorImpl。
public Editor edit() {
// TODO: remove the need to call awaitLoadedLocked() when
// requesting an editor. will require some work on the
// Editor, but then we should be able to do:
//
// context.getSharedPreferences(..).edit().putString(..).apply()
//
// ... all without blocking.
synchronized (mLock) {//所以如果 sp 沒有創(chuàng)建财松,也是無法寫入內(nèi)容的
awaitLoadedLocked();
}
return new EditorImpl();
}
接著調(diào)用 EditorImpl 的 putString() 方法闯袒,會(huì)將key 和 value 存入Map 中:
//EditorImpl 中
@GuardedBy("mLock")
private final Map<String, Object> mModified = Maps.newHashMap();
public Editor putString(String key, @Nullable String value) {
synchronized (mLock) {
mModified.put(key, value);
return this;
}
}
接著調(diào)用 commmit() 方法 或者 apply(),寫入xml 文件中:
//commit() 方法
public boolean commit() {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
MemoryCommitResult mcr = commitToMemory();
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);
return mcr.writeToDiskResult;
}
//apply() 方法
public void apply() {
final long startTime = System.currentTimeMillis();
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
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() {
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);
}
這里需要關(guān)注的就是游岳,先會(huì)創(chuàng)建一個(gè) MemoryCommitResult 對象政敢,其構(gòu)造方法如下:
private MemoryCommitResult(long memoryStateGeneration, @Nullable List<String> keysModified,
@Nullable Set<OnSharedPreferenceChangeListener> listeners,
Map<String, Object> mapToWriteToDisk) {
this.memoryStateGeneration = memoryStateGeneration;
this.keysModified = keysModified;
this.listeners = listeners;//SP ChangeListener
this.mapToWriteToDisk = mapToWriteToDisk;//存儲(chǔ)了需要寫入xml文件的的key-value 的map
}
接著會(huì)將該 MemoryCommitResult 封裝到 Runnable 中,接著最后調(diào)用 QueuedWork.queue() 執(zhí)行磁盤io 操作胚迫。
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
final boolean isFromSyncCommit = (postWriteRunnable == null);
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
// Typical #commit() path with fewer allocations, doing a write on
// the current thread.
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
QueuedWork 是封裝了一個(gè) HandlerThread 的類喷户,所以,如果在該類執(zhí)行访锻,也就等于在子線程執(zhí)行 IO褪尝,commit() 和 apply() 的區(qū)別在于:
-
在調(diào)用 QueuedWork.queue() 方法的時(shí)候,apply() 是 postDelay() 100毫秒執(zhí)行的期犬。
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); } } }
如果是 apply() 則會(huì) postDelay()
-
其次的區(qū)別在于apply() 會(huì)觸發(fā) QueuedWork.addFinisher(awaitCommit)河哑,如下:
//apply() 方法中 QueuedWork.addFinisher(awaitCommit);
那么這里會(huì)導(dǎo)致 waitToFinish,在 QueueWork.java 中:
/** * Trigger queued work to be processed immediately. The queued work is processed on a separate * thread asynchronous. While doing that run and process all finishers on this thread. The * finishers can be implemented in a way to check weather the queued work is finished. * * Is called from the Activity base class's onPause(), after BroadcastReceiver's onReceive, * after Service command handling, etc. (so async work is never lost) */ public static void waitToFinish() { long startTime = System.currentTimeMillis(); boolean hadMessages = false; Handler handler = getHandler(); synchronized (sLock) { if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) { // Delayed work will be processed at processPendingWork() below handler.removeMessages(QueuedWorkHandler.MSG_RUN); if (DEBUG) { hadMessages = true; Log.d(LOG_TAG, "waiting"); } } // We should not delay any work as this might delay the finishers sCanDelay = false; } StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites(); try { processPendingWork(); } finally { StrictMode.setThreadPolicy(oldPolicy); } try { while (true) { Runnable finisher; synchronized (sLock) { finisher = sFinishers.poll(); } if (finisher == null) { break; } finisher.run(); } } finally { sCanDelay = true; } synchronized (sLock) { long waitTime = System.currentTimeMillis() - startTime; if (waitTime > 0 || hadMessages) { mWaitTimes.add(Long.valueOf(waitTime).intValue()); mNumWaits++; if (DEBUG || mNumWaits % 1024 == 0 || waitTime > MAX_WAIT_TIME_MILLIS) { mWaitTimes.log(LOG_TAG, "waited: "); } } } }
根據(jù)官方解釋龟虎,在 Activity.onStop() ,BroadCastReceiver.onReceive(),Service handleCommend() 的時(shí)候璃谨,都會(huì)去執(zhí)行這個(gè) waitToFinish(),保證數(shù)據(jù)不會(huì)丟失。
例如在 Activity.onStop() 的時(shí)候鲤妥,會(huì)調(diào)用以下代碼:
//ActivityThread.java 中 private void handleStopActivity(IBinder token, boolean finished, boolean userLeaving, int configChanges, boolean dontReport, int seq) { ....... // Make sure any pending writes are now committed. if (!r.isPreHoneycomb()) { QueuedWork.waitToFinish(); } ........ }
也就是需要處理完你之前 apply() 提交的內(nèi)容佳吞,該 Activity 才會(huì) onStop(),但是實(shí)際上,如果是啟動(dòng)新的 Activity棉安,好像不會(huì)有問題底扳,但是如果是回退當(dāng)前 Activity 的話,可能會(huì)因?yàn)?SP 的 apply() 操作贡耽,卡主當(dāng)前 Activity 的生命周期衷模。
那么為什么非要 waitToFinish() 呢?因?yàn)槲覀兪褂?Activity 作為 Context 操作一個(gè) SP蒲赂,那么實(shí)際上如果沒有確認(rèn)該 Activity 不會(huì)再次操作 SP阱冶,那么新舊 Activity 同時(shí)操作 SP 那么這種情況下,非常容易出錯(cuò)凳宙,而且會(huì)影響效率熙揍。
SharedPreferences ANR 避免方案
- 自定義一個(gè) SharedPreferencesImpl,去除 WorkQueue 的 waiteFinish() 的相關(guān)邏輯
- 代理 Activity 和 Application 的 getSharedPerfrnences() 方法氏涩,返回自定義的 SharedPreferencesImpl
- 盡量不要寫入大的 key-value 值届囚,對 key-value 進(jìn)行強(qiáng)制檢查有梆,例如在 putString() 進(jìn)行長度檢查
- 不要同時(shí)多次 apply()
- 盡量在子線程讀取sp,然后返回到主線程意系,在操作sp