Synopsis
如果有使用到 SharedPreference 的同学,采用 apply 操作来异步提交数据到本地的话,可能就会在崩溃平台上遇到这种 ANR:
DALVIK THREADS (62):
"main" prio=5 tid=1 Waiting
| group="main" sCount=1 dsCount=0 obj=0x76eb4a20 self=0x7f8f09a000
| sysTid=20196 nice=0 cgrp=default sched=0/0 handle=0x7f9333beb0
| state=S schedstat=( 49400463809 21820262476 111431 ) utm=4144 stm=796 core=0 HZ=100
| stack=0x7fd4bdc000-0x7fd4bde000 stackSize=8MB
| held mutexes=
at java.lang.Object.wait!(Native method)
- waiting on <0x39ae9327> (a java.lang.Object)
at java.lang.Thread.parkFor(Thread.java:1220)
- locked <0x39ae9327> (a java.lang.Object)
at sun.misc.Unsafe.park(Unsafe.java:299)
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:363)
at android.app.QueuedWork.waitToFinish(QueuedWork.java:88)
at android.app.ActivityThread.handleStopActivity(ActivityThread.java:3900)
at android.app.ActivityThread.access$1200(ActivityThread.java:187)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1614)
at android.os.Handler.dispatchMessage(Handler.java:111)
at android.os.Looper.loop(Looper.java:192)
at android.app.ActivityThread.main(ActivityThread.java:5886)
at java.lang.reflect.Method.invoke!(Native method)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1031)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:826)
我们通过解析 SharedPreferenceImpl 源码来看问题是怎么出现的。
Analysis
final class SharedPreferencesImpl implements SharedPreferences {
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false; // 判断文件是否加载完成
mMap = null; //
startLoadFromDisk();
}
// 根据传进来的 file 的路径,创建一个 mBackupFile
static File makeBackupFile(File prefsFile) {
return new File(prefsFile.getPath() + ".bak");
}
private void startLoadFromDisk() {
synchronized (mLock) { // 将状态置为未加载状态
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
}
构造函数里面主要做2件事,根据传进来的 file 创建一个 mBackupFile,然后在子线程中调用 loadFromDisk()。
private void loadFromDisk() {
synchronized (mLock) {
if (mLoaded) { // 这句为什么不放到锁外面?
return;
}
if (mBackupFile.exists()) { // 如果存再备份文件,则优先使用备份文件
mFile.delete();
mBackupFile.renameTo(mFile);
}
}
Map map = null;
StructStat stat = null;
try {
stat = Os.stat(mFile.getPath());
if (mFile.canRead()) {
BufferedInputStream str = null;
try {
// 将文件里的数据都出来,存放到 map 中
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; // 加载完成
if (map != null) {
mMap = map;
mStatTimestamp = stat.st_mtime;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
mLock.notifyAll(); // 唤醒等待线程
}
}
可以看出,这个方法就是读取传进来的 file 文件里的数据并存放到 map 中。构造完成后就可以读数据了:
public long getLong(String key, long defValue) {
synchronized (mLock) {
awaitLoadedLocked();
// 从 mMap 中读取数据返回
Long v = (Long)mMap.get(key);
return v != null ? v : defValue;
}
}
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) { // 如果没加载完就等待
try {
mLock.wait();
} catch (InterruptedException unused) {
}
}
}
可以看出读取操作会先判断数据是否已经从本地读取到内存,如果没有的话,会等待。当数据读取完成后就会调用 mLock.notifyAll(),这时就会将 key 对应的 value 从 map 中返回。 再来看下写操作,写操作包括 commit() 和 apply() 进行提交。先看 commit():
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) {
awaitLoadedLocked();
}
return new EditorImpl();
}
public final class EditorImpl implements Editor {
private final Object mLock = new Object();
@GuardedBy("mLock")
private final Map<String, Object> mModified = Maps.newHashMap();
public Editor putStringSet(String key, @Nullable Set<String> values) {
synchronized (mLock) {
// 写操作是单独存放到一个新的 mModified 中,而不是前面的 mMap
mModified.put(key,
(values == null) ? null : new HashSet<String>(values));
return this;
}
}
public boolean commit() {
long startTime = 0;
MemoryCommitResult mcr = commitToMemory(); // 先提交到内存
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */); // 然后提交到硬盘
try {
// 这里就是导致 ANR 的核心,后面会讲
mcr.writtenToDiskLatch.await(); // 等硬盘写完
} catch (InterruptedException e) {
return false;
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
调用 context.getSharedPreferences(..).edit().putString(..).commit() 的 edit() 方法会返回一个 EditorImpl 对象。然后调用该方法的 putxxx() 将数据保存到一个新的 HashMap 中。然后调用 commit() 方法将数据提交到内存和硬盘中。我们先来看下这个提交到内存的方法是干啥的:
// 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) { // 加 mLock 锁代表写内存的时候不允许读
// We optimistically don't make a deep copy until
// a memory commit comes in when we're already
// writing to disk.
if (mDiskWritesInFlight > 0) {
// We can't modify our mMap as a currently
// in-flight write owns it. Clone it before
// modifying it.
// noinspection unchecked
mMap = new HashMap<String, Object>(mMap);
}
mapToWriteToDisk = mMap;
mDiskWritesInFlight++;
synchronized (mLock) { //对当前的Editor加锁
boolean changesMade = false;
if (mClear) { // 当调用了 clear() 方法后,mClear 才为 true
if (!mMap.isEmpty()) {
changesMade = true;
mMap.clear(); // 清空mMap。mMap里面存的是整个的Preferences
}
mClear = false;
}
// 遍历 mModified,并将新数据添加到 mMap 中
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.
if (v == this || v == null) { // 调用 remove 方法时,v 被设置为 this
if (!mMap.containsKey(k)) {
continue;
}
mMap.remove(k);
} else {
if (mMap.containsKey(k)) {
Object existingValue = mMap.get(k);
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
mMap.put(k, v); // 将 put 进来的有效值存放到 mMap 中
}
changesMade = true; // 表示 mMap 中的数据又被修改
}
mModified.clear();
if (changesMade) {
mCurrentMemoryStateGeneration++;
}
memoryStateGeneration = mCurrentMemoryStateGeneration;
}
}
// 返回 MemoryCommitResult 对象,该对象是用于存放这些数据的
return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
mapToWriteToDisk);
}
commit 操作就是将 put 到 mModified 中的数据存放到 mMap 中。来看下 MemoryCommitResult:
private static class MemoryCommitResult {
final long memoryStateGeneration;
@Nullable final List<String> keysModified;
@Nullable final Set<OnSharedPreferenceChangeListener> listeners;
final Map<String, Object> mapToWriteToDisk;
final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);
@GuardedBy("mWritingToDiskLock")
volatile boolean writeToDiskResult = false;
boolean wasWritten = false;
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;
this.mapToWriteToDisk = mapToWriteToDisk;
}
// 外部调用 writtenToDiskLatch.await() 来等待 enqueueDiskWrite 执行完成,
// 然后通过调用这个方法来继续执行后面的操作
void setDiskWriteResult(boolean wasWritten, boolean result) {
this.wasWritten = wasWritten;
writeToDiskResult = result;
writtenToDiskLatch.countDown();
}
}
这个类里面主要是由一个 CountDownLatch,在上面 commit() 中,在执行写硬盘操作后会调用 mcr.writtenToDiskLatch.await() 来等待写操作完成。我们来看写硬盘操作:
/**
* Enqueue an already-committed-to-memory result to be written
* to disk.
*
* They will be written to disk one-at-a-time in the order
* that they're enqueued.
*
* @param postWriteRunnable if non-null, we're being called
* from apply() and this is the runnable to run after
* the write proceeds. if null (from a regular commit()),
* then we're allowed to do this disk write on the main
* thread (which in addition to reducing allocations and
* creating a background thread, this has the advantage that
* we catch them in userdebug StrictMode reports to convert
* them where possible to apply() ...)
*/
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
// commit() 方法传进来的为 null,此时为 true
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();
}
}
};
// commit() 方法会执行这里,它在当前线程中执行,占用更少的内存
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) { // 当前只有一个批次等待写入
writeToDiskRunnable.run(); // 直接在当前线程执行
return;
}
}
// 如果调用的是 apply() 或者当前有多个批次等待写入,那么另起线程写入
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
前面传进来的第二个参数为 null,那么会执行 if 语句,如果当前只有一个批次等待写入,就会在当前线程执行 writeToDiskRunnable 调用 writeToFile 方法。否则就调用 QueuedWork.queue() 。我们来看下 writeToFile:
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;
boolean fileExists = mFile.exists();
// 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; // commit 会调用到这里
} 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 (!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);
try {
final StructStat stat = Os.stat(mFile.getPath());
synchronized (mLock) {
mStatTimestamp = stat.st_mtime;
mStatSize = stat.st_size;
}
} catch (ErrnoException e) {
// Do nothing
}
// 写成功后,备份文件就不需要了
mBackupFile.delete();
// 更新 mDiskStateGeneration 用于上面的判断
mDiskStateGeneration = mcr.memoryStateGeneration;
// 硬盘写完了后,这里就会调用 countDown() 方法来释放锁
mcr.setDiskWriteResult(true, true);
long fsyncDuration = fsyncTime - writeTime;
mSyncTimes.add(Long.valueOf(fsyncDuration).intValue());
mNumSync++;
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);
}
这里主要就是将变更写到硬盘中,写完后就会调用 mcr.setDiskWriteResult(true, true) 来执行 countDown()。我们再来看 apply() 方法:
public void apply() {
final long startTime = System.currentTimeMillis();
// 和 commit 一样,将数据从 mModified 存放到 mMap 中
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
public void run() {
try {
mcr.writtenToDiskLatch.await(); // 等待写硬盘
} catch (InterruptedException ignored) {
}
}
};
// 产生 ANR 的关键
QueuedWork.addFinisher(awaitCommit);
// 这个 runnable 就是为了执行上面的 awaitCommit Runnable,从而调用 mcr.writtenToDiskLatch.await()
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);
}
我们来看下 enqueueDiskWrite():
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
final boolean isFromSyncCommit = (postWriteRunnable == null); // 此时为 false
// 调用 writeToFile 来写硬盘,并调用 mcr.writtenToDiskLatch.await() 来等待写入完成
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
// 执行 mcr.writtenToDiskLatch.await() 来等待写硬盘完成
postWriteRunnable.run();
}
}
};
...
// 此时直接将该 runnable 传到 QueuedWork 中
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
可以看出 apply() 方法就是将一个 runnable 传给 QueuedWork,这个 runnable 是用来执行写硬盘操作,并且会阻塞当前线程。
public class QueuedWork {
public static void queue(Runnable work, boolean shouldDelay) {
Handler handler = getHandler();
synchronized (sLock) {
sWork.add(work); // 将 runnable 添加到 sWork 中
// 发送消息
if (shouldDelay && sCanDelay) {
handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
} else {
handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
}
}
}
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;
}
}
private static class QueuedWorkHandler extends Handler {
static final int MSG_RUN = 1;
QueuedWorkHandler(Looper looper) {
super(looper);
}
public void handleMessage(Message msg) {
if (msg.what == MSG_RUN) {
processPendingWork();
}
}
}
private static void processPendingWork() {
long startTime = 0;
synchronized (sProcessingWork) {
LinkedList<Runnable> work;
synchronized (sLock) {
work = (LinkedList<Runnable>) sWork.clone(); // 取出前面放进去的 Runnable 列表
sWork.clear();
// Remove all msg-s as all work will be processed now
getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
}
if (work.size() > 0) {
for (Runnable w : work) {
// 在 HandlerThread 中执行写硬盘操作,并调用 mcr.writtenToDiskLatch.await()
// 来等待写完成
w.run();
}
}
}
}
}
可以看出 apply() 方法就是将写硬盘操作放到 HandlerThread 中异步执行的。
到此,SharedPreferenceImpl 源码都分析完了,大致就是创建 SharedPreferenceImpl 时会去本地读取文件中所有的数据到 mMap,中,后面的读取操作就是直接从 mMap 中获取 value。写操作会先写到 mModified 的 HashMap 中,然后将其写到 mMap 中,再将 mMap 写到硬盘。写硬盘要区分 commit() 操作还是 apply() 操作,commit() 操作是在当前线程中写到硬盘中,而 apply 操作会将写硬盘操作封装到 Runnable 交给 QueuedWork 中的 Handler 在 HandlerThread 中异步执行。
我们再回到上面的 ANR 问题,出现 ANR 的堆栈信息为:
at java.util.concurrent.CountDownLatch.await(CountDownLatch.java:202)
at android.app.SharedPreferencesImpl$EditorImpl$1.run(SharedPreferencesImpl.java:363)
at android.app.QueuedWork.waitToFinish(QueuedWork.java:88)
at android.app.ActivityThread.handleStopActivity(ActivityThread.java:3900)
我们来看下 waitToFinish():
/**
* 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() {
...
try {
// 这个方法里面会等待所有的 runnable 执行完
processPendingWork();
} finally {
StrictMode.setThreadPolicy(oldPolicy);
}
try {
while (true) {
Runnable finisher;
// 将前面 apply() 放进来的封装有 mcr.writtenToDiskLatch.await() 的 runnalbe 取出来
synchronized (sLock) {
finisher = sFinishers.poll();
}
if (finisher == null) {
break;
}
// 在当前线程等待,导致 ANR
finisher.run();
}
} finally {
sCanDelay = true;
}
}
private static void processPendingWork() {
long startTime = 0;
// 当 下面的 run 方法没执行完,则会一直等它执行完
synchronized (sProcessingWork) {
LinkedList<Runnable> work;
synchronized (sLock) {
work = (LinkedList<Runnable>) sWork.clone();
sWork.clear();
// Remove all msg-s as all work will be processed now
getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
}
if (work.size() > 0) {
for (Runnable w : work) {
w.run();
}
}
}
}
可以看出 waitToFinish 会一直等待 run 方法执行完,所以虽然 run 是在 HandlerThread 中执行的,它依然会阻塞主线程。所以问题就是 Activity 在 stop 的时候要在当前线程等待子线程中所有的 runnable 执行完成导致 ANR。
而这些 runnable 是通过 apply 提交到 sFinishers 中,或者是当前在处理 commit 提交的 runnable,此时又 commit 了,也会将 commit 的 runnable 提交到 sFinishers。
solution
可以看出解决办法就是避免提交 runnable 到 sFinishers 中。那么只能使用 commit,而且每次 commit 未执行完成不能执行另一个 commit 方法。这种业务逻辑就可以考虑 HandlerThread 之类的,将 runnable 都放到消息队列中,然后执行完当前 runnable 后再执行下一个。
参考文献
- android-26 源码
- Android-SharedPreferences源码学习与最佳实践
- 一个由SHAREDPREFERENCES引起的ANR