稀有猿诉

十年磨一剑,历炼出锋芒,说话千百句,不如码二行。

Android Sync Barrier机制

诡异的假死问题

前段时间,项目上遇到了一个假死问题,随机出现,无固定复现规律,大量频繁随机操作后,便会出现假死,整个应用无法操作,不会响应事件,会发生各种奇怪的ANR,且trace不固定。非常之诡异。

经过大量的复现研究和分析, 以及大神的指点后,发现与同步屏障(Sync Barrier)有关系,于是发现有必要研究一下这个东西。

什么是Sync Barrier机制

这是安卓线程消息队列里面的一个新增加的东西,这么说还是太抽象,我们从头说起这件事情:

安卓的消息队列机制

消息队列,或者叫做Event Loop,通常在任何一个GUI应用程序里面都会有的,应用大部分时间处于Idle状态,当有事件发生时,比如用户点了一个button,然后开始响应此事件。安卓也是一个GUI应用程序,绝大多数都是带有GUI的应用程序,那么安卓 里面是如何实现这个EventLoop的呢,它是用Looper和MessageQueue,以及Handler,以一种消息队列的方式来实现loop。

有一定经验的同学对这些东西肯定不陌生,因为它们在实际的开发过程中相当常见,比如说对于UI的操作只能放在主线程里面,那么当工作线程想要更新UI时就需要用Handler发一个消息,或者post一个Runnable。或者当你想延后一段时间执行某种操作,就可以用postDelayed。这些都是非常常规的操作了。对于工作线程,如果想启用消息队列,就用Looper#prepare就可以了,当然了,要记得quit。

内部原理上面也不是很复杂,就是Looper会给线程绑定一个消息队列,即是MessageQueue,这是一个无限循环的队列,不断的轮询队列,当有新的消息时就去处理,否则就等待。主线程,安卓框架层在创建应用进程的时候就会给主线程默认创建好MessageQueue,所以就可以向其发消息(sendMessage)或者postDelayed,它们本质上都是一样的,都是向MessageQueue中入队一个消息,稍后它便会得到处理。

同步消息与异步消息

这个MessageQueue机制,就是队列,也就是说符合队列的特点,先进先出(FIFO,First-In First Out),就是说你先post的消息,肯定是先被处理,后post的后处理,即使有delay时候,也是看谁先到,谁先到谁先被处理。因此,这里面的消息全是同步,也就是说所有消息都是顺序处理,这就是同步消息。

异步消息,也就是说某个消息,想被最高优先级处理,无视发送消息的时机,比如说队列里面有8个消息,如何想让某个消息最先被处理?这时队列就变成了优先队列,有优先级的队列。那么具有高优先级的消息也是异步消息(Asynchronous Message)。即使是最后加入队列的,但因为是异步消息,它会被先处理,并不是FIFO,此可理解 为异步。

Sync Barrier用以实现优先队列

说了这么多,Sync Barrier就是安卓 内部用以实现优先级队列的一种方式。

当队列中出现Sync barrier(具体实现上就是Message#target为null)时,就会忽略所有同步消息,寻找异步消息(isAsynchrouns为true)的消息,然后优先处理它。

需要注意的是,把消息标记为异步,以及向消息队列中发送Sync barrier,这些API全部都是hide的,也就是说app中是无法使用的,通过反射也许能调用成功,但风险也较大,后续会被谷歌限制调用。换言之,这东西只能在Frameworks层内部自己使用。

为什么要有Sync Barrier

说了这么多,其实本质上,这东西就是一个优先队列,给要处理的消息加一个优先级机制,那这有什么实际用途呢?

消息队列这东西是在安卓一诞生就有了的东西,大部分时候它也没有什么问题。但有一个事情,就是安卓操作系统的UI流畅度远不及水果平台(iOS),原因就是在于水果平台的UI渲染是整个系统中最高优先执行。

有同学会说安卓里面也是这样啊,你想UI都只能在主线程里面操作(因此主线程也叫UI线程)。只能在主线程中操作UI,就能保证UI渲染是最高优先级吗?当然不是了。因为整个应用程序的默认线程就是主线程,换句话说,如果你不明显的去做线程切换,或者启用工作线程,那么所有事情都发生在主线程里面,当然 也包括了UI渲染,因此UI的渲染与你在主线程时面post一个消息的优先级是一样的。

如何让UI渲染在主线程中以最高优先级运行?于是就有了Sync barrier机制,这东西就是为了让消息队列有优先级,并且没有开放给app使用。可以去看一下ViewRootImpl(这货是专门负责ViewTree渲染的,也即可以理解为负责UI渲染的)的几个perform,它都是异步消息,也即会开启Sync barrier,它发送的消息将会是最高优先级的,会被优先处理。

主要在哪里用Sync barrier

前面提到了,Sync barrier这玩意儿并不是给app开发同学用的,很多相关的接口并没有开放出来,这是为了提高UI渲染而设计的东西。因此这东西主要是用在了UI渲染过程中。

仔细查看ViewRootImpl的源码可以发现,每次渲染View tree之前都会先给主线程插入一个Sync barrier,以挡住同步消息,以保证渲染被主线程优先执行到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}

void unscheduleTraversals() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
        mChoreographer.removeCallbacks(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
    }
}

void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

        performTraversals();
   }
}

这里的逻辑略复杂一些,View tree本身的处理过程,也即三大步measure, layout和draw,也就是performTraversal本身并没有异步消息,它是在准备渲染的时候放一个sync barrier,而在具体处理每一帧前就移除了sync barrier,这里为何要这样,还没有完全想清楚。通过搜索ViewRootImpl可以发现只有input event,keyevent 以及与用户输入相关的消息被设置为了asynchronous,也就是说用户事件响应被提高了优先级,而view tree的渲染,即UI的每一帧,其实并没有被提升优先级。因为UI刷的每一帧是以固定频率刷新的,Choreographer 从硬件得到vsync脉冲信号,然后回调给ViewRootImpl让其渲染每一帧(也即是performTraversal)。

Sync Barrier会引发什么问题

说实话,这套机制,实现的并不怎么优雅,因为,毕竟它并不是在最初的设计之初就考虑到的东西,它的整体运行机制并不完善,非常依赖于调用者的使用,所以它的相关API并未有开放出来。

它有三步,先发一个Sync barrier,然后发送异步消息,然后再移除Sync barrier。

只有UI渲染(ViewTree的相关操作,才需要这样做),大部分其他的消息都是同步的,并不需要这样搞。当有Sync barrier时,消息队列在处理消息的时候会忽略掉所有的同步消息(也即是常规消息),优先处理异步消息,直到Sync barrier移除,也是需要手动移除的。Sync barrier需要手动移除是最坑的。

因此,假如要处理的异步特别多,或者逻辑出错Sync barrier没有被移除,那就悲剧 了,就会导致消息队列中的大量常规消息无法得到处理,队列就会停止工作,应用会出现随机的ANR,以及假死。

如何调试

很不幸,Sync barrier导致的问题很难调试,甚至很难被发现,通常都是ANR或者说卡死问题。

那么首先可以按照ANR和卡死的常规分析方式去分析,假如都未发现明显的问题时,比如没有明显的耗时的操作,也没有死锁,也没有被硬件和IO阻塞,也没有进入死循环。

这些常规的分析,都没有发现问题。这时就可以考虑是不是Sync barrier在搞鬼。特别当涉及一些诡异的UI状态时,比如某个View只显示 了一半,比如某一个View没有显示 完全,比如只有背景没有前景,等等,当排除了其他常规问题时,就很可能是Sync barrier有异常导致的。

另外,如果有能力修改Frameworks的话,可以给MessageQueue增加dump信息,把队列中的所有消息都打印出来,以及把Sycn barrier也都打印出来,这样能够比较清楚看到,队列内部的情况,自然也能够发现异常的Sync barrier。

如何避免Sync Barrier搞鬼

前面提到过,这套东西都是Frameworks层内部的机制,并没有开放给app使用,而Frameworks内部的逻辑一般来说还是相当健壮的,绝大多数时候并不会出问题。当然了,各个厂商内部搞的各种所谓优化,倒是有可能会引发问题。

在实际开发过程中,引发Sync barrier的最多场景就是自定义View。对于自定义View,是能够在非主线程调用其invalidate的,当有大量的非主线程调用invalidate时,就有可能恰好与主线程的渲染发生交互,具体case非常corner要刚巧非主线程在postInvalide,然后主线程也刚巧在发送异步消息,就可能使得Sync barrier没有被移除,从而导致问题。

这就需要我们在编码阶段做好封装,对于自定义View的刷新触发逻辑做好封装,做一下线程切换,以保证是在主线程里面执行invalidate。因为暴露出去的接口,是没有办法控制的,你没有办法让所有调用者都在主线程里面调用你的接口。

参考资料

Comments