关于android:深入浅出Android同步屏障机制

1次阅读

共计 4704 个字符,预计需要花费 12 分钟才能阅读完成。

原文链接 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,以挡住同步音讯,以保障渲染被主线程优先执行到。

    @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。因为裸露进来的接口,是没有方法管制的,你没有方法让所有调用者都在主线程外面调用你的接口。

参考资料

  • Handler sync barrier(同步屏障)
  • Android 同步屏障机制(Sync Barrier)/)
  • 同步屏障?阻塞唤醒?和我一起重读 Handler 源码
  • 同步屏障与异步音讯,从入门到放弃
  • 面试官:如何进步 Message 的优先级
  • 今日头条 ANR 优化实际系列 – Barrier 导致主线程假死

原创不易,打赏 点赞 在看 珍藏 分享 总要有一个吧

正文完
 0