面试官问我:SharedPreference源码中apply跟commit的原理,导致ANR的原因

1.前言

好几年前写过一篇SharedPreference源码相关的文章,对apply跟commit方法讲解的不够透彻,作为颜值担当的天才少年来说,怎么能不一次深入到底呢?

2.正文

为了熟读源码,下班后我约了同事小雪一起探讨,毕竟三人行必有我师焉。哪里来的三个人,不管了,跟小雪研究学术更重要。
小安学长,看了你之前的文章: Android SharedPreference 源码分析(一)对apply(),commit()的底层原理还是不理解,尤其是线程和一些同步锁他里面怎么使用,什么情况下会出现anr?

既然说到apply(),commit()的底层原理,那肯定是老步骤了,上源码。apply源码如下:

publicvoidapply() {
            finallongstartTime= System.currentTimeMillis();

            finalMemoryCommitResultmcr= commitToMemory();
            finalRunnableawaitCommit=newRunnable() {
                    publicvoidrun() {
                        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);

            RunnablepostWriteRunnable=newRunnable() {
                    publicvoidrun() {
                        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源码如下所示:

privatevoidenqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
        finalbooleanisFromSyncCommit= (postWriteRunnable == null);

        finalRunnablewriteToDiskRunnable=newRunnable() {
                publicvoidrun() {
                    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) {
            booleanwasEmpty=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源码:

publicstaticvoidqueue(Runnable work, boolean shouldDelay) {
        Handlerhandler= getHandler();

        synchronized (sLock) {
            sWork.add(work);

            if (shouldDelay && sCanDelay) {
                handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
            } else {
                handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
            }
        }
    }
复制代码
privatestaticclassQueuedWorkHandlerextendsHandler {
        staticfinalintMSG_RUN=1;

        QueuedWorkHandler(Looper looper) {
            super(looper);
        }

        publicvoidhandleMessage(Message msg) {
            if (msg.what == MSG_RUN) {
                processPendingWork();
            }
        }
    }
复制代码

这边我默认你已经知道HandlerThread如何使用啦,如果不知道,麻烦花五分钟去看下我之前的博客。

上面的代码很简单,其实就是把writeToDiskRunnable这个任务放到sWork这个list中,并且执行handler,根据HandlerThread的知识点,我们知道handlermessage里面就是子线程了。

接下来我们继续看handleMessage里面的processPendingWork()方法:

privatestaticvoidprocessPendingWork() {
        longstartTime=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方法的代码:

finalRunnablewriteToDiskRunnable=newRunnable() {
                publicvoidrun() {
                    synchronized (mWritingToDiskLock) {
                        writeToFile(mcr, isFromSyncCommit);//由于handlermessage在子线程,则writeToFile也在子线程中
                    }
                    synchronized (mLock) {
                        mDiskWritesInFlight--;
                    }
                    if (postWriteRunnable != null) {
                        postWriteRunnable.run();
                    }
                }
            };
复制代码

writeToFile方法我们不深入去看,但是要关注,里面有个setDiskWriteResult方法,在该方法里面做了如下的事情:

voidsetDiskWriteResult(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。

privatevoidhandlePauseActivity(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();到底做了什么

publicstaticvoidwaitToFinish() {
        longstartTime= System.currentTimeMillis();
        booleanhadMessages=false;

        Handlerhandler= 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.ThreadPolicyoldPolicy= 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) {
            longwaitTime= 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()方法里面调用的,代码如下:

@Overridepublicvoidapply() {
            ......
            // 将 awaitCommit 添加到队列 QueuedWork 中
            QueuedWork.addFinisher(awaitCommit);

            RunnablepostWriteRunnable=newRunnable() {
                    @Overridepublicvoidrun() {
                        awaitCommit.run();
                        QueuedWork.removeFinisher(awaitCommit);// 将 awaitCommit 从队列 QueuedWork 中移除
                    }
                };
            ......
        }
复制代码

正常情况下其实是不会发生ANR的,因为writeToDiskRunnable方法中,是先进行文件存储再去阻塞等待的,此时CountDownLatch永远都为0,则不会阻塞主线程。

finalRunnablewriteToDiskRunnable=newRunnable() {
                @Overridepublicvoidrun() {
                    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数组对象是什么,并且执行什么动作。

privatestaticfinal LinkedList<Runnable> sFinishers = newLinkedList<>();
   @UnsupportedAppUsagepublicstaticvoidaddFinisher(Runnable finisher) {
        synchronized (sLock) {
            sFinishers.add(finisher);
        }
    }
复制代码

addFinisher刚刚上面提到是在apply方法中调用,则finisher就是入参awaitCommit,他的run方法如下:

finalRunnableawaitCommit=newRunnable() {
                    @Overridepublicvoidrun() {
                        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线程中,我们看下代码证实一下:

@Overridepublicbooleancommit() {
            longstartTime=0;

            if (DEBUG) {
                startTime = System.currentTimeMillis();
            }

            MemoryCommitResultmcr= commitToMemory();//内存保存

            SharedPreferencesImpl.this.enqueueDiskWrite(
                mcr, null/* sync write on this thread okay */);//第二个参数为nulltry {
                mcr.writtenToDiskLatch.await();
            } catch (InterruptedException e) {
                returnfalse;
            } 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方法:

privatevoidenqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
        finalbooleanisFromSyncCommit= (postWriteRunnable == null);//此时postWriteRunnable为null,isFromSyncCommit 则为truefinalRunnablewriteToDiskRunnable=newRunnable() {
                @Overridepublicvoidrun() {
                    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则为truebooleanwasEmpty=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的原理,只是今晚有点晚了,咱们早上睡吧,不是,早点回家吧~~~

作者:天才少年_

链接:https://juejin.cn/post/7209447968218382392

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值