SharedPreferences调用导致的ANR分析
转自:http://blog.chinaunix.net/uid-29506893-id-5761774.html
ANR文件提取的有用片段如下:
----- pid 13431 at 2016-09-14 11:46:10 -----
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)
可以看到这里面卡在waitToFinish()上面,该函数的等待导致了ANR的发生,看其函数描述如下:
点击(此处)折叠或打开
-
/**
-
* Finishes or waits for async operations to complete.
-
* (e.g. SharedPreferences$Editor#startCommit writes)
-
*
-
* 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() {
-
Runnable toFinish;
-
while ((toFinish = sPendingWorkFinishers.poll()) != null) {
-
toFinish.run();
-
}
- }
解释为在处理activity、service或广播的时候会被执行,以确保之前加入等待队列sPendingWorkFinishers的异步处理能够执行到结束。果然这里确实被ActivityThread调用,整体函数交互如下:
可以看到写入磁盘等待awaitCommit在apply中被创建,同时加入了QueuedWork(即apply方法会将等待写入到文件系统的任务放在QueuedWork的等待完成队列里)。awaitCommit阻塞在await(判断CountDownLatch为0时返回)。
正常情况下equeueDiskWrite()函数创建线程执行writeToDiskRunnable:
- 当正常写入成功后,在setDiskWriteResult()会将CountDownLatch减值为0;
- 进一步调用postWriteRunnable执行删除QueueWork中的任务。
函数调用如下:
点击(此处)折叠或打开
-
private void enqueueDiskWrite(final MemoryCommitResult
mcr,
-
final Runnable postWriteRunnable) {
-
final Runnable writeToDiskRunnable = new Runnable() {
-
public void run() {
-
synchronized (mWritingToDiskLock) {
-
writeToFile(mcr);
//写入文件,最终调用mcr.setDiskWriteResult()设置writtenToDiskLatch.countDown();
-
}
-
synchronized (SharedPreferencesImpl.this) {
-
mDiskWritesInFlight--;
-
}
-
if (postWriteRunnable != null) {
-
postWriteRunnable.run(); //这里实际执行的awaitCommit,执行时由于之前countDown操作使得count=0从而await()会直接返回
-
}
-
}
-
};
-
......
-
QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
- }
从这里可以梳理出为什么调用QueueWork.waitToFinish()为什么会出现等待:
sPendingWorkFinshers中存在未完成的任务(awaitCommit) <=== postWriteRunnable还没有机会执行(因为执行后在队列中是被删除的) <=== writeToFile()执行未结束(从而也就没有机会去减少CountDownLatch的值) <=== awaitCommit阻塞在CountDownLatch;
看到上面的推论,其中writeToFile最耗时间,在这个异步操作过程中,我们的Activity结束了,然后调用waitToFinish()将会导致hang住。
所以如果我们使用SharedPreference的apply方法, 虽然该方法可以很快返回, 并在其它线程里将键值对写入到文件系统, 但是当Activity的onPause/onStop等方法被调用时,会等待写入到文件系统的任务完成,如果写入比较慢,主线程就会出现ANR问题。
另外,SharedPreference除了提供apply外还提供commit方法,源码如下所示,该方法直接在调用线程中执行,不会转入后台,但如果我们在UI线程commit,且磁盘写入较慢的情况下,ANR依然会发生,这个时候就需要将修改SharedPreference值的任务放到单独线程里去做。
点击(此处)折叠或打开
-
public boolean commit() {
-
MemoryCommitResult mcr = commitToMemory();
-
SharedPreferencesImpl.this.enqueueDiskWrite(
-
mcr, null /* sync write on this thread okay */);
-
try {
-
mcr.writtenToDiskLatch.await();
-
} catch (InterruptedException e) {
-
return false;
-
}
-
notifyListeners(mcr);
-
return mcr.writeToDiskResult;
- }
所以,使用SharedPreference时无论使用apply还是commit都会有很小的概率由于写入磁盘问题(IO瓶颈)出现ANR。除了IO瓶颈外,锁瓶颈也是SharedPreference的一个缺点,单个共享数据的文件在被同时操作时会有加解锁开销。
总结一下,由于IO瓶颈导致的ANR我们是束手无策的,但我们在使用过程可以优化:
1)对于多个线程访问单个SharedPreference的情况用apply比较好(提高响应速度),只有一个线程访问的话用commit比较好; 2)对于修改多个值能一次提交; 3)如果真的发现调用经常出现ANR,那就考虑起一个线程在后台来做,这个时候选用commit,而不是apply。
附上一篇文章,其粗略总结了使用SharedPreference的缺点和注意事项:
http://www.cnblogs.com/puff/p/5530825.html