您的位置:首页 > 移动开发 > Android开发

Android SharedPreferences.apply() 问题

2017-11-21 21:54 501 查看
最近APP发现一个ANR错误, 日志如下. 特记录.

错误日志

bgAnr=false hs=47e6a6320faafa601b5821f46a16eef5 at java.lang.Object.wait!(Native method)
// 在等待需要的锁
waiting on <0x0da52e06> (a java.lang.Object)
at java.lang.Thread.parkFor$(Thread.java:2160)
locked <0x0da52e06> (a java.lang.Object)
at sun.misc.Unsafe.park(Unsafe.java:325)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:161)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:840)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireSharedInterruptibly(AbstractQueuedSynchronizer.java:994)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireSharedInterruptibly(AbstractQueuedSynchronizer.java:1303)
at java.util.concurrent.CountDownLatch.await(CountDownLatch.java:203)
at android.app.SharedPreferencesImpl$EditorImpl$1.run(SharedPreferencesImpl.java:376)
at android.app.QueuedWork.waitToFinish(QueuedWork.java:109)
at android.app.QueuedWork.waitToFinish(QueuedWork.java:100)
at android.app.ActivityThread.handleStopActivity(ActivityThread.java:4027)
at android.app.ActivityThread.-wrap25(ActivityThread.java:-1)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1585)
at android.os.Handler.dispatchMessage(Handler.java:102)


根据上面的错误信息, 可以得到如下两点:

由于线程锁没有释放引起.

相关的类 : QueuedWork, SharedPreferencesImpl$EditorImpl, ActivityThread.

接下来开始依次查看相关的源码

查看 ActivityThread.handleStopActivity() 方法源码

// onPause 时会调用
private void handleStopActivity(IBinder token, boolean show, int configChanges, int seq) {
// other code ...

// Make sure any pending writes are now committed.
// 确保所有的磁盘写操作都得到执行.
if (!r.isPreHoneycomb()) {
// 找到了调用.根据QueuedWork中的注释得知该功能是确保所有的磁盘写任务都执行完.
QueuedWork.waitToFinish();
}
// other coder
// ...
}

// isPreHoneycomb 位于 Activity.java 中.
// Activity == null || 大于Android 3.0 (api 11, honeycomb)
public boolean isPreHoneycomb() {
if (activity != null) {
return activity.getApplicationInfo().targetSdkVersion
< android.os.Build.VERSION_CODES.HONEYCOMB;
}
return false;
}


接下来就需要查看QueuedWork 的源码, 看看QueuedWork.waitToFinish() 的实现.

查看 QueuedWork 类源码

/**
* Internal utility class to keep track of process-global work that's
* outstanding(未完成的,杰出的,显著的) and hasn't been finished yet.
*
* This was created for writing SharedPreference edits out
* asynchronously so we'd have a mechanism(机制) to wait for the writes in
* Activity.onPause and similar places, but we may use this mechanism
* for other things in the future.
*
* 大致意思就是为了确保SharedPreference的写操作真正写入磁盘的类.
* @hide
*/
public class QueuedWork {

// The set of Runnables that will finish or wait on any async
// activities started by the application.
// 阻塞的 awaiteCommit 任务.
private static final ConcurrentLinkedQueue<Runnable> sPendingWorkFinishers =
new ConcurrentLinkedQueue<Runnable>();

private static ExecutorService sSingleThreadExecutor = null; // lazy, guarded by class

/**
* Returns a single-thread Executor shared by the entire process,
* creating it if necessary.
* 线程池, 异步的磁盘写任务就是在这个线程中执行的.
*/
public static ExecutorService singleThreadExecutor() {
synchronized (QueuedWork.class) {
if (sSingleThreadExecutor == null) {
// TODO: can we give this single thread a thread name?
sSingleThreadExecutor = Executors.newSingleThreadExecutor();
}
return sSingleThreadExecutor;
}
}

/**
* Add a runnable to finish (or wait for) a deferred operation
* started in this context earlier.  Typically finished by e.g.
* an Activity#onPause.  Used by SharedPreferences$Editor#startCommit().
*
* Note that this doesn't actually start it running.  This is just
* a scratch set for callers doing async work to keep updated with
* what's in-flight.  In the common case, caller code
* (e.g. SharedPreferences) will pretty quickly call remove()
* after an add().  The only time these Runnables are run is from
* waitToFinish(), below.
*/
public static void add(Runnable finisher) {
sPendingWorkFinishers.add(finisher);
}

public static void remove(Runnable finisher) {
sPendingWorkFinishers.remove(finisher);
}

/**
* Finishes or waits for async operations to complete.
* (e.g. SharedPreferences$Editor#startCommit wr
e337
ites)
*
* 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() {
// 等待所有的awaitCommit 执行完, 也就是确保磁盘写完成.
Runnable toFinish;
while ((toFinish = sPendingWorkFinishers.poll()) != null) {
toFinish.run();
}
}

/**
* Returns true if there is pending work to be done.  Note that the
* result is out of data as soon as you receive it, so be careful how you
* use it.
*/
public static boolean hasPendingWork() {
return !sPendingWorkFinishers.isEmpty();
}

}


通过分析该源码可以知道:

1. QueuedWork 目前是用来确保SharedPrefenced的写操作在Activity 销毁前执行完的一个全局队列.

2. QueuedWork#sPendingWorkFinishers 用来保存没有完成的操作的Runnable. 通过后面的分析我们得知其实Runnable就是在等待写操作完成的锁.一旦写操作完成,释放了锁.

3. QueuedWork#singleThreadExecutor 一个只有一个线程的线程池, 任务顺序执行.其实这里面保存的就是真正的写磁盘的任务.

4. QueuedWork.waitToFinish() 遍历执行sPendingWorkFinishers中的任务.

其实到这里已经大概知道了UI线程阻塞的原因 : 就是singleThreadExecutor中有没有执行的写磁盘任务, 导致对应的sPendingWorkFinishers中的阻塞任务没有执行完.在Activity销毁时会调用sPendingWorkFinishers中的所有的没有执行的任务的run方法导致UI线程阻塞.

既然是SharedPreferences的写磁盘操作. 那么就是commit或者apply中添加的任务.查看相关源码

commit()源码

// commit
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;
}


apply()源码

/**
* apply方法.
*/
public void apply() {
// 提交添加到内存,这也是apply可以立即获取的原因.
final MemoryCommitResult mcr = commitToMemory();
// 这个Task是用来等待对应的写磁盘操作完成的任务.
// 也就是放在QueuedWork中的.在最终UI线程也是
// 阻塞在这个Task中的.
final Runnable awaitCommit = new Runnable() {
public void run() {
try {
// 等待磁盘写入成功,否则阻塞在这.
// 系统在将本次保存写到磁盘上后会设置writtenToDiskLatch锁-1. 这样的话下面的阻塞代码就可以转型过了.
// 同样如果没有真正的执行写磁盘操作执行这个Task的线程就会阻塞在这.
// writeToFile() 方法是真正的写磁盘操作.
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
}
};

// 添加到全局的工作队列中.
QueuedWork.add(awaitCommit);

// 这个任务主要是等待磁盘写入完成然后讲阻塞任务从全局任务中一处.
Runnable postWriteRunnable = new Runnable() {
public void run() {
awaitCommit.run();
QueuedWork.remove(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()来调度磁盘写

/**
* 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) {
// 写磁盘任务.
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
// 写磁盘.该放发中国会调用 writtenToDiskLatch.countDown();
writeToFile(mcr);
}
synchronized (SharedPreferencesImpl.this) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
// 写磁盘成功后则执行移除全局队列中的任务的任务.
// 此时waitCommit 任务就不会阻塞了, 因为writtenToDiskLatch==0 了.
// 不阻塞 QueuedWork.remove(awaitCommit); 就会被调用, 也就是说在任务执行完了
// 就会讲该任务从全局队列中移除.
postWriteRunnable.run();
}
}
};

// 判断是否是同步写任务.异步:apply, 同步: commit
final boolean isFromSyncCommit = (postWriteRunnable == null);

// Typical #commit() path with fewer allocations分配, doing a write on
// the current thread.
if (isFromSyncCommit) {
boolean wasEmpty = false;
// mDiskWritesInFlight 会在commitToMemory() 方法中进行+1 操作.
synchronized (SharedPreferencesImpl.this) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
// 在当前线程执行写任务.
writeToDiskRunnable.run();
// 执行完写操作直接返回.
return;
}
}

// 在其他线程中执行写磁盘操作.
QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}


MemoryCommitResult

// Return value from EditorImpl#commitToMemory()
private static class MemoryCommitResult {
public boolean changesMade;  // any keys different?
public List<String> keysModified;  // may be null
public Set<OnSharedPreferenceChangeListener> listeners;  // may be null
public Map<?, ?> mapToWriteToDisk;
public final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);
public volatile boolean writeToDiskResult = false;

public void setDiskWriteResult(boolean result) {
writeToDiskResult = result;
writtenToDiskLatch.countDown();
}
}


通过上面的4段源码可以得出结论:

commit是同步执行的. apply() 是异步执行的.

根据1中的结论, 得出在commit的情况下.写磁盘在当前线程中就完成了.也就会讲QueuedWork#sSingleThreadExecutor中的对应的阻塞任务移除了.

apply() 会讲写任务添加到QueuedWork#singleThreadExecutor线程池中. 所以执行时间会延后.

结论 :

apply() 方法虽然会自动讲任务放到其他线程中执行.开发者不需要关系调用起来也方便,但是在某些情况下可能会导致UI线程阻塞造成ANR. 特别是高IO的APP.

apply() 方法的执行流程大致如下 :

1. 创建 awaitCommit 该任务是阻塞等待磁盘操作完成. 添加到全局队列QueuedWork中.
2. 创建postWriteRunnable 该任务是会等待磁盘操作完成然后一处全局队列中的阻塞任务 awaitCommit.
3. 创建writeToDiskRunnable 该任务是执行真正的写磁盘操作的任务, 同事在写完后调用 postWriteRunnable.run()
将 awaitCommit 全局队列QueuedWork中移除.

4. 执行情况
4.1 同步执行commit
在这种情况下,会在创建 writeToDiskRunnable 任务后直接调用 run 方法, 在当前线程中执行磁盘操作和移除
QueuedWork中的阻塞任务的操作.也就是说commit的所有操作都在当前线程执行.
4.2 异步执行apply
这种情况下. 会将 writeToDiskRunnable 跑到 QueuedWork 中的一个线程线程池中执行. 至于什么时间执行
就是 QueuedWork 来安排了.反正不在当前线程.

如果在Activity销毁时磁盘写任务还没有执行完. 则UI线程就是执行 awaitCommit.run() 任务, 由于在磁盘写没有完成的
情况下会 awaitCommit.run() 方法是阻塞的. 就会造成UI线程阻塞, 就有可能产生ANR.


解决办法:

使用commit() 方法.自己进行异步处理.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  android