美文网首页Android开发经验谈Android开发
原生SharedPreferences ANR问题的分析

原生SharedPreferences ANR问题的分析

作者: Joe_blake | 来源:发表于2020-06-08 17:24 被阅读0次

    一、Android8.0以下SharedPreferences任务调度的实现

    • SharedPreferencesImpl.apply()的实现:
    public void apply() {
            //创建一个result,它内部有一个计数器(初始值为1)
        final MemoryCommitResult mcr = commitToMemory();
        final Runnable awaitCommit = new Runnable() {
                public void run() {
                    try {
                            //阻塞并等待计数器归零
                        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);
    }
    

    每次进行apply操作时,创建了两个runnable:

    1. awaitCommit被添加到QueuedWork的finishers队列中:执行该runnable需要等待MemoryCommitResult中的计数器归零------即任务写入完成;
    2. postWriteRunnable
      1. 会被放入到一个单线程的线程池中执行:QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
      2. 执行结束后,会remove掉QueuedWork中被添加的awaitCommit;
    3. MemoryCommitResult.writtenToDiskLatch何时归零:执行写入操作writeToFile()方法完成后;
    • QueuedWork:SharedPreferences的任务标记执行队列
      • finisher队列:sPendingWorkFinishers-------保存awaitCommit的队列
      • 单线程池:sSingleThreadExecutor------执行写入操作postWriteRunnable的线程池
      • 产生ANR的源头:waitToFinish()方法
    /**
    * 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();
        }
    }
    

    该方法会循环的取出sPendingWorkFinishers中的runnable,并等待全部执行完成;

    • 在ActivityThread消息队列的处理

      以下是需要需要等待waitToFinish()方法执行完成,ActivityThread.H中的处理事件:

    SERVICE_ARGS------handleServiceArgs()
    STOP_SERVICE-------handleStopService()
    PAUSE_ACTIVITY_FINISHING-------handlePauseActivity()
    STOP_ACTIVITY_HIDE------handleStopActivity()
    SLEEPING--------handleSleeping()
    

    问题明了:

    执行完apply()方法后,会产生一一对应的awaitCommit和postWriteRunnable,postWriteRunnable执行完成后会清除掉MemoryCommitResult中的计数器,并从删除QueuedWork.sPendingWorkFinishers中对应的awaitCommit。

    即:当线程池sSingleThreadExecutor中写入任务未被全部完成,QueuedWork中sPendingWorkFinishers队列就不为空,QueuedWork.waitToFinish()方法就会依次执行sPendingWorkFinishers队队列中的任务------awaitCommit,awaitCommit在写入操作完成,会被writtenToDiskLatch阻塞。此时如果ActivityThread要处理以上事件,UI线程被waitToFinish()方法block,就有可能发生ANR。

    解决思路:实现一个自定义的ConcurrentLinkedQueue,重写poll()方法强制返回null,然后动态代理掉QueuedWork中的sPendingWorkFinishers,waitToFinish()方法就不会产生阻塞。

    二、Android8.0及以上上SharedPreferences任务调度的实现

    我们先来看一下官方优化的改动点:

    1. 将原来的单线程的线程池修改为HandlerThread;
    
    2. QueuedWork中同时持有sWork工作队列和sFinishers锁队列,并将任务队列由ConcurrentLinkedQueue改为LinkedList实现; 
    3. 将写入文件的工作从原来直接抛到线程池中,改为直接移动到QueueWork中的sWork中。
    

    SharedPreferencesImpl.apply()的实现与之前差异不大,核心在于QueuedWork的变化

    新QueuedWork源码分析:

    • 可延迟 Runnable 的延迟值。
    /** Delay for delayed runnables, as big as possible but low enough to be barely perceivable */
    private static final long DELAY = 100; 
    
    • waitToFinish() 运行超过 MAX_WAIT_TIME_MILLIS 毫秒,发出警告
    private static final long MAX_WAIT_TIME_MILLIS = 512; 
    
    • 本类使用的锁对象
    private static final Object sLock = new Object();
    
    • 执行任务时的锁对象
    private static Object sProcessingWork = new Object();
    
    • 两个队列:
    //任务链表
    private static final LinkedList<Runnable> sWork = new LinkedList<>();
    //存放 Finisher 的链表
    private static final LinkedList<Runnable> sFinishers = new LinkedList<>();
    
    • 执行任务的handler
    @GuardedBy("sLock")
    private static Handler sHandler = null;
    
    • 新任务是否能被延迟,默认为 true
    @GuardedBy("sLock")
    private static boolean sCanDelay = true;
    
    • QueuedWorkHandler相关实现:
    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 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;
        }
    }
    

    这是一个运行在HandlerThread上的handler;

    • 任务入队:
    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);
            }
        }
    }
    

    任务入队并执行,判断delay or not,执行processPendingWork();

    由apply()触发的入队操作,shouldDelay都为true,即sCanDelay为true时,均延迟100ms执行;即默认情况下,processPendingWork()每100ms,触发一次打包执行操作--------将写入操作进行分片处理,每次只处理100ms内被添加进的任务;

    • 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. 清空handler中的消息,lsCanDelay = false;
    2. 执行processPendingWork();
    3. 循环执行sFinishers队列中的finisher,等在sFinishers中的锁集合的执行,即等待HandlerThread线程执行完写入操作;

    由于processPendingWork()方法中的写入操作是加锁的(sProcessingWork),所以UI线程和HandlerThread触发的写入任务不会同时进行--------即当UI线程触发waitToFinish()时,需要等待HandlerThread线程执行写入操作完成,释放sProcessingWork锁,然后处理下一个100ms内等待执行的写入任务,而后续再被添加进的任务,UI线程不会再处理,而是继续交由HandlerThread线程执行;

    waitToFinish()中任务执行前,会将sCanDelay置位false,processPendingWork()执行完成后,置为true。即当在UI线程触发waitToFinish()方法的过程中,如果此时再有写入工作,会向HandlerThread发送没有delay的写入操作消息,会在ui线程执行完成写入操作后,立即执行。

    • 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");
                }
            }
        }
    }
    

    执行逻辑:

    1. clone工作队列sWork,并清空sWork;
    2. 清空QueueWork中的HandlerThread的消息队列中的MSG_RUN消息;
    3. 依次执行任务队列中的runnable;

    procressPendingWork的执行入口有两个:

    1. handler在HandlerThread线程处理MSG_RUN消息;
    2. UI线程执行waitToFinish,

    执行时会处理所有当前消息时间节点之前,sWorker队列中所有已存在的任务,所以需要清空队列中的MSG_RUN消息。

    优化思路:对sWork进行动态代理,复写链表的clone和clear方法:当clone方法是UI线程调用时,返回一个空的集合,避免主线程执行写入文件的操作导致block。当clear方法被UI调用时,不做清空直接return。实际上将原来UI线程处理的写入任务,交给HandlerThread线程写入。

    相关文章

      网友评论

        本文标题:原生SharedPreferences ANR问题的分析

        本文链接:https://www.haomeiwen.com/subject/mdortktx.html