SharedPreferences anr 原因以及避免方案
技术背景:
AuthMode 和SDK 使用了系统默认的 SharedPreferences,系统的 SharedPreferences 实现类在 android/app/SharedPrefenencesImpl.java 中。
然后出现了类似这样的 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 工作流程
我们平时这样获取一个 SharedPrefenences(全文使用 SP 作为SharedPrefenences的简称)
context.getSharedPreferences("", Context.MODE_PRIVATE)
然后,无论是 Activity 和 Application 对应的 Context,其实现链路如下:
//直接调用的是 ContextWrapper 里的方法 在 ContextWarpper.java 中
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
return mBase.getSharedPreferences(name, mode);
}
// mBase 对象是 ContextImpl 的实例,在 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();
//首先会从 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;
}
那么获取一个 SharedPreferences 的流程为:
-
根据传入的 SharedPreferences 名称,去读取对应的文件,如果文件不存在则创建文件,默认的路径在 data/data/your package name/shared_prefs 文件夹下,也就是改代码创建的路径。
new File(getDataDir(), "shared_prefs");
获取文件之后,调用 getSharedPreferencesCacheLocked() 获取在 static ArrayMap 该应用包名缓存的一个 SharedPreferences ArrayMap。然后查看是否有该文件对应的缓存 SharedPerferences。
-
如果没有缓存的 SP,则会创建一个,调用代码如下:
sp = new SharedPreferencesImpl(file, mode);
将sp 缓存到第二步获得的 ArrayMap,这是一个 static 的变量。
那么接下来讲下,SP IO 操作的相关流程:
在上面的方法中,实际创建 SharedPreferences 的是这一行代码:
sp = new SharedPreferencesImpl(file, mode);
那么对应的构造方法如下:
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
startLoadFromDisk();
}
实际上先会这里开启了子线程进行了IO操作:
//SharedPreferencesImpl.java
private void startLoadFromDisk() {
synchronized (mLock) {//对mLock 加锁
mLoaded = false;//注意这个变量
}
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();//这里会唤醒所有的等待锁
}
}
所以在主线程调用 getSharedPreferences() ,会开启子线程去IO 操作File,这是没问题的,但是如果你调用 SP 的 getXXX() 方法的时候,就可能有问题了,且看下面的分析。
读取 SharedPreferences 导致 ANR 的根本原因
例如你从sp 中读取一个 String,会调用到 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 对象创建完成,mLoaded 才会是 true
BlockGuard.getThreadPolicy().onReadFromDisk();
}
while (!mLoaded) {
try {
mLock.wait();
} catch (InterruptedException unused) {
}
}
}
那么这里会调用 awaitLoadedLocked() 直到该 SP 对象创建完成,所以这里就对导致主线程等待。从上面知道,只有 SP 对应的xml 解析完了,并且创建出 SP 对象,mLoaded 才会是 true,否则就会一直等待。如果你存储的 SP 特别大,那么可能就会导致主线程 ANR。
SharedPreferences 性能代价
从上面的分析知道,我们的 SP 读取过一次之后,就会在一个 static 的 ArrayMap 中存储起来,如下:
//ContextImpl.java 中
@GuardedBy("ContextImpl.class")
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
那么实际上要避免这种操作,避免存储超大的字符串。
SharedPreferences commit() 和 apply() 原理
我们往 sp 写入内容,一般如下:
sp.edit().putString("","").commit()
其中 edit() 方法获取的是一个Editor,其实现类是 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 没有创建,也是无法写入内容的
awaitLoadedLocked();
}
return new EditorImpl();
}
接着调用 EditorImpl 的 putString() 方法,会将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;
}
}
接着调用 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);
}
这里需要关注的就是,先会创建一个 MemoryCommitResult 对象,其构造方法如下:
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;//存储了需要写入xml文件的的key-value 的map
}
接着会将该 MemoryCommitResult 封装到 Runnable 中,接着最后调用 QueuedWork.queue() 执行磁盘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 是封装了一个 HandlerThread 的类,所以,如果在该类执行,也就等于在子线程执行 IO,commit() 和 apply() 的区别在于:
-
在调用 QueuedWork.queue() 方法的时候,apply() 是 postDelay() 100毫秒执行的。
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() 则会 postDelay()
-
其次的区别在于apply() 会触发 QueuedWork.addFinisher(awaitCommit),如下:
//apply() 方法中 QueuedWork.addFinisher(awaitCommit);
那么这里会导致 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: "); } } } }
根据官方解释,在 Activity.onStop() ,BroadCastReceiver.onReceive(),Service handleCommend() 的时候,都会去执行这个 waitToFinish(),保证数据不会丢失。
例如在 Activity.onStop() 的时候,会调用以下代码:
//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() 提交的内容,该 Activity 才会 onStop(),但是实际上,如果是启动新的 Activity,好像不会有问题,但是如果是回退当前 Activity 的话,可能会因为 SP 的 apply() 操作,卡主当前 Activity 的生命周期。
那么为什么非要 waitToFinish() 呢?因为我们使用 Activity 作为 Context 操作一个 SP,那么实际上如果没有确认该 Activity 不会再次操作 SP,那么新旧 Activity 同时操作 SP 那么这种情况下,非常容易出错,而且会影响效率。
SharedPreferences ANR 避免方案
- 自定义一个 SharedPreferencesImpl,去除 WorkQueue 的 waiteFinish() 的相关逻辑
- 代理 Activity 和 Application 的 getSharedPerfrnences() 方法,返回自定义的 SharedPreferencesImpl
- 尽量不要写入大的 key-value 值,对 key-value 进行强制检查,例如在 putString() 进行长度检查
- 不要同时多次 apply()
- 尽量在子线程读取sp,然后返回到主线程,在操作sp