记得看文章三部曲,点赞,谈论,转发。 微信搜索【程序员小安】关注还在移动开发领域苟活的大龄程序员,“面试系列”文章将在大众号同步发布。
1.前语
好几年前写过一篇SharedPreference源码相关的文章,对apply跟commit办法解说的不行透彻,作为颜值担任的天才少年来说,怎样能不一次深化究竟呢?
2.正文
为了熟读源码,下班后我约了同事小雪一起讨论,毕竟三人行必有我师焉。哪里来的三个人,不管了,跟小雪研讨学术更重要。
小安学长,看了你之前的文章:Android SharedPreference 源码剖析(一)对apply(),commit()的底层原理仍是不理解,尤其是线程和一些同步锁他里边怎样运用,什么情况下会呈现anr?
已然说到apply(),commit()的底层原理,那必定是老过程了,上源码。 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");
}
}
};
// 将 awaitCommit 添加到行列 QueuedWork 中
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);// 将 awaitCommit 从行列 QueuedWork 中移除
}
};
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);
}
你这丢了一大堆代码,我也看不懂啊。
别急啊,这漫漫长夜留给咱们的工作很多啊,听我一点点给你讲,包你满意。
apply()办法做过安卓的都知道(假如你没有做过安卓,那你点开我博客干什么呢,死走不送),频繁写文件建议用apply办法,由于他是异步存储到本地磁盘的。那么具体源码是怎么操作的,让咱们掀开他的底裤,不是,让咱们透过外表看实质。
咱们从下往上看,apply办法最后调用了SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);我长得帅我先告诉你,enqueueDiskWrite办法会把存储文件的动作放到子线程,具体怎样放的,咱们等下看源码,这边你只要知道他的作用。这个办法的第二个参数
postWriteRunnable做了两件事:
1)让awaitCommit履行,及履行 mcr.writtenToDiskLatch.await();
2)履行QueuedWork.remove(awaitCommit);代码
writtenToDiskLatch是什么,QueuedWork又是什么?
writtenToDiskLatch是CountDownLatch的实例化目标,CountDownLatch是一个同步工具类,它经过一个计数器来实现的,初始值为线程的数量。每逢一个线程完结了自己的使命调用countDown(),则计数器的值就相应得减1。当计数器到达0时,表明一切的线程都已履行结束,然后在等候的线程await()就能够康复履行使命。
1)countDown(): 对计数器进行递减1操作,当计数器递减至0时,当时线程会去唤醒堵塞行列里的一切线程。
2)await(): 堵塞当时线程,将当时线程参加堵塞行列。
能够看到假如postWriteRunnable办法被触发履行的话,由于 mcr.writtenToDiskLatch.await()的原因,UI线程会被一直堵塞住,等候计数器减至0才能被唤醒。
QueuedWork其实便是一个基于handlerThread的,处理使命行列的类。handlerThread类为你创立好了Looper和Thread目标,创立Handler的时分运用该looper目标,则handleMessage办法在子线程中,能够做耗时操作。假如对于handlerThread的不熟悉的话,能够看我前面的文章:Android HandlerThread运用介绍以及源码解析
enqueueDiskWrite源码如下所示:
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);
}
很明显postWriteRunnable不为null,程序会履行QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);从writeToDiskRunnable咱们能够看到,他里边做了两件事:
1)writeToFile():内容存储到文件;
2)postWriteRunnable.run():postWriteRunnable做了什么,往上看,上面现已讲了该办法做的两件事。
QueuedWork.queue源码:
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);
}
}
}
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();
}
}
}
这边我默认你现已知道HandlerThread怎么运用啦,假如不知道,麻烦花五分钟去看下我之前的博客。
上面的代码很简单,其实便是把writeToDiskRunnable这个使命放到sWork这个list中,而且履行handler,依据HandlerThread的知识点,咱们知道handlermessage里边便是子线程了。
接下来咱们持续看handleMessage里边的processPendingWork()办法:
private static void processPendingWork() {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
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();
}
if (DEBUG) {
Log.d(LOG_TAG, "processing " + work.size() + " items took " +
+(System.currentTimeMillis() - startTime) + " ms");
}
}
}
}
这代码同样很简单,先是把sWork克隆给work,然后敞开循环,履行work目标的run办法,及调用writeToDiskRunnable的run办法。上面讲过了,他里边做了两件事:1)内容存储到文件 2)postWriteRunnable办法回调。 履行run办法的代码:
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr, isFromSyncCommit);//由于handlermessage在子线程,则writeToFile也在子线程中
}
synchronized (mLock) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
writeToFile办法咱们不深化去看,可是要关注,里边有个setDiskWriteResult办法,在该办法里边做了如下的工作:
void setDiskWriteResult(boolean wasWritten, boolean result) {
this.wasWritten = wasWritten;
writeToDiskResult = result;
writtenToDiskLatch.countDown();//计数器-1
}
怎么上面仔细看了的同学,应该能够知道,当调用countDown()办法时,会对计数器进行递减1操作,当计数器递减至0时,当时线程会去唤醒堵塞行列里的一切线程。也便是说,当文件写完时,UI线程会被唤醒。
已然文件写完就会释放锁,那什么情况下会呈现ANR呢?
Android体系为了保障在页面切换,也便是在多进程中sp文件能够存储成功,在ActivityThread的handlePauseActivity和handleStopActivity时会经过waitToFinish保证这些异步使命都现已被履行完结。假如这个时分过渡运用apply办法,则或许导致onpause,onStop履行时间较长,然后导致ANR。
private void handlePauseActivity(IBinder token, boolean finished,
boolean userLeaving, int configChanges, boolean dontReport, int seq) {
......
r.activity.mConfigChangeFlags |= configChanges;
performPauseActivity(token, finished, r.isPreHoneycomb(), "handlePauseActivity");
// Make sure any pending writes are now committed.
if (r.isPreHoneycomb()) {
QueuedWork.waitToFinish();
}
......
}
你必定要问,为什么过渡运用apply办法,就有或许导致ANR?那咱们只能看QueuedWork.waitToFinish();究竟做了什么
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: ");
}
}
}
}
看着一大坨代码,其实做了两件事:
1)主线程履行processPendingWork()办法,把之前未履行完的内容存储到文件的操作履行完,这部分动作直接在主线程履行,假如有未履行的文件操作而且文件较大,则主线程会由于IO时间长形成ANR。
2)循环取出sFinishers数组,履行他的run办法。假如这时分有多个异步线程或许异步线程时间过长,同样会形成堵塞发生ANR。
第一个很好理解,第二个没有太看理解,sFinishers数组是在什么时分add数据的,而且依据writeToDiskRunnable办法能够知道,先写文件再加锁的,为啥会堵塞呢?
sFinishers的addFinisher办法是在apply()办法里边调用的,代码如下:
@Override
public void apply() {
......
// 将 awaitCommit 添加到行列 QueuedWork 中
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);// 将 awaitCommit 从行列 QueuedWork 中移除
}
};
......
}
正常情况下其实是不会发生ANR的,由于writeToDiskRunnable办法中,是先进行文件存储再去堵塞等候的,此刻CountDownLatch永远都为0,则不会堵塞主线程。
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr, isFromSyncCommit);//写文件,写成功后会调用writtenToDiskLatch.countDown();计数器-1
}
synchronized (mLock) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();//回调到awaitCommit.run();进行堵塞
}
}
};
可是假如processPendingWork办法在异步线程在履行时,及经过enqueueDiskWrite办法触发的正常文件保存流程,这时分文件比较大或许文件比较多,子线程则一直在运行中;当用户点击页面跳转时,则触发该Activity的handlePauseActivity办法,依据上面的剖析,handlePauseActivity办法里边会履行waitToFinish保证这些异步使命都现已被履行完结。
由于这边主要介绍循环取出sFinishers数组,履行他的run办法形成堵塞发生ANR,咱们就重点看下sFinishers数组目标是什么,而且履行什么动作。
private static final LinkedList<Runnable> sFinishers = new LinkedList<>();
@UnsupportedAppUsage
public static void addFinisher(Runnable finisher) {
synchronized (sLock) {
sFinishers.add(finisher);
}
}
addFinisher刚刚上面说到是在apply办法中调用,则finisher便是入参awaitCommit,他的run办法如下:
final Runnable awaitCommit = new Runnable() {
@Override
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");
}
}
};
不难看出,便是调用CountDownLatch目标的await办法,堵塞当时线程,将当时线程参加堵塞行列。也便是这个时分整个UI线程都堵塞在这边,等候processPendingWork这个异步线程履行结束,尽管你是在子线程,可是我主线程在等你履行结束才会进行页面切换,所以假如过渡运用apply办法,则或许导致onpause,onStop履行时间较长,然后导致ANR。
小安学长不愧是我的偶像,我都理解了,那持续讲讲同步存储commit()办法吧。
commit办法其实就比较简单了,无非是内存和文件都在UI线程中,咱们看下代码证实一下:
@Override
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 */);//第二个参数为null
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;
}
能够看到enqueueDiskWrite的第二个参数为null,enqueueDiskWrite办法其实上面解说apply的时分现已贴过了,为了不让你往上翻咱们持续看enqueueDiskWrite办法:
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
final boolean isFromSyncCommit = (postWriteRunnable == null);//此刻postWriteRunnable为null,isFromSyncCommit 则为true
final Runnable writeToDiskRunnable = new Runnable() {
@Override
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) { //当调用commit办法时,isFromSyncCommit则为true
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();//主线程回调writeToDiskRunnable的run办法,进行writeToFile文件的存储
return;
}
}
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
关键代码现已注释过了,由于postWriteRunnable为null,则isFromSyncCommit为true,代码会在主线程回调writeToDiskRunnable的run办法,进行writeToFile文件的存储。这部分动作直接在主线程履行,假如文件较大,则主线程也会由于IO时间长形成ANR的。
所以SharedPreference 不管是commit()仍是apply()办法,假如文件过大或许过多,都会有ANR的风险,那怎么躲避呢?
处理必定有办法的,下一篇就介绍SharedPreference 的替代计划mmkv的原理,仅仅今晚有点晚了,咱们早上睡吧,不是,早点回家吧~~~