原生SharedPreferences ANR问题的分析

一、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线程写入。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335