意思-一文读懂Android中的屏幕刷新机制

Hi,我是小余。本文已收录到 GitHub · Androider-Planet 中。这里有 Android 进阶成长知识体系,关注公众号 [小余的自习室

] ,在成功的路上不迷路!

很多同学觉得屏幕刷新绘制知识点对他们开发不重要,没必要学习这些东西,这部分同学可能平时维护的是一些中小型项目或者应用是安装在特定设备上,只要求写写主界面,做一些简单的网络请求,业务交互相关知识,对性能这块要求不是很高,确实涉及不到太多屏幕刷新这块知识。

又不是不能用.jpg

但对一些大中型项目来说可能就不一样了:他们涉及业务较多,设备种类较多,往往一个app内部集成了十几个子业务甚至上百个,这对应用性能要求就更加严格了,app的体验也会间接导致用户的留存问题。

所以学习屏幕绘制这类理论性较强的知识也是非常有必要的。 如果你想进阶成为高级开发:屏幕绘制这块知识也是一个绕不过去的坎。

前面一篇卡顿优化的文章我们说过,主流屏幕刷新频率是每秒60次(高的有90,120等),也就是16.6ms刷新一次屏幕, 如果我们在主线程做一些耗时的操作,最直观的现象就是屏幕卡顿,其实就是发生了丢帧。

由此抛出几个问题: - 1.16.6ms是什么意思,每次16.6ms都会调用一个绘制流程么? - 2.为什么在主线程做一些耗时操作会出现卡顿?丢帧? - 3.丢帧是个什么意思,是字面上的直接丢弃当前帧还是延后显示当前帧? - 4.双缓冲是什么?有了双缓存就不会出现丢帧了么?三缓冲呢? - 5.了解Vsync么?它的作用是什么? - 6.画面撕裂是怎么造成的? - 7.编舞者是个什么东西?

带着这些问题我们出发吧。CPU:,主要用于计算数据,在Android中主要用于三大绘制流程中Surface的计算过程,起着生产者的作用GPU:,主要用于游戏画面的渲染,在Android中主要用于将CPU计算好的Surface数据合成后放到buffer中,让显示器进行读取,起着消费者的作用。

如下图:

屏幕刷新过程.gif

其中GPU在架构中是以服务的形式工作

SurfaceFlinger作用是接受多个来源的图形显示数据Surface,合成后发送到显示设备。

比如我们的主界面中:可能会有statusBar,侧滑菜单,主界面,这些View都是独立Surface渲染和更新,最后提交给SF后,SF根据Zorder,透明度,大小,位置等参数,合成为一个数据buffer,传递HW

cpu和gpu.webp

屏幕在刷新buffer的时候,并不是一次性扫描完成,而是从左到右,从上到下的一个读取过程,顺序显示一屏的每个像素点,你为什么看不到,因为太快了嘛,按60HZ的屏幕刷新率来算,这个过程只有16.66666...ms。

在视频领域中,帧就代表一张图片。玩过短视频剪辑的朋友应该对此很了解。

图中为放大后的一帧图片。

帧截图.jpg

而帧率和屏幕刷新率确是两个不同的概念: - 帧率:表示GPU在1s中内可以渲染多少帧到buffer中,单位是fps,这里要理解的是帧率是一个动态的,比如我们平时说的60fps,只是1s内最多可以渲染60帧,假如我们屏幕是静止的,则GPU此时就没有任何操作,帧率就为0. - 屏幕刷新率:屏幕在1s内去buffer中取数据的次数,单位为HZ,常见屏幕刷新率为60HZ。和帧率不一样,屏幕刷新率是一个固定值和硬件参数有关。

画面撕裂简单说就是显示器把多个帧显示在同一个画面中。如图:

屏幕撕裂.webp

画面撕裂的原因:我们知道屏幕刷新率是固定的,假设为60HZ,正常情况下当我们的GPU的帧率也是60fps的时候,GPU绘制完一帧,屏幕刷新一帧,这样是不会出问题的,但是随着GPU显卡性能的提升,GPU的帧率超过60fps后,就会出现画面撕裂的情况,实际在帧率较低的时候也会出现撕裂的情况。

所以其本质是帧率和屏幕刷新率的不一致导致的撕裂。

那可能大家要说了,等屏幕一帧刷新完成后,再将新的一帧存到buffer中不就可以了,那你要知道,早期的4.0之前设备是只有一个buffer,且其并没有buffer同步的概念,屏幕读取buffer中的数据时,GPU是不知道的,屏幕读取的同时,GPU也在写入,导致buffer被覆盖,出现同一画面使用的是不同帧的数据。

那既然是因为使用同一个Buffer引起的画面撕裂,使用两个buffer不就可以了?

前面我们说到画面撕裂是由于单buffer引起的,在4.1之前,使用了双缓冲来解决画面撕裂。GPU写入的缓存为:屏幕刷新使用的缓存为:

如下图:

双buffer.png

因为使用双buffer,屏幕刷新时,frame buffer不会发生变化,通过交换buffer来实现帧数据切换,那什么时候交换buffer呢?

这就要引入Vsync的概念了。

我们知道如果一个屏幕在刷新的过程中,是不能交换buffer的,只有等屏幕刷新完成后以后才可以考虑buffer的交换.

那具体什么时候交换呢?。

此时硬件屏幕会发出一个脉冲信号,告知GPU和CPU可以交换了,这个就是Vsync信号。

有了双缓冲和VSync是不是就都ok了?虽然上面方式可以解决屏幕撕裂的问题,但是还是会出现一些其他问题。

双缓冲buffer交换还有个前提就是GPU已经准备好了back buffer的数据,如果在Vsync到来时back buffer并没有准备好,就不会进行缓存的交换,屏幕显示的还是前一帧画面,这种情况就是Jank。

有了上面的基础我们再来聊聊Android屏幕刷新机制的演变过程

Android屏幕刷新机制演变过程按buffer的个数可以分为3个阶段: - 1. - 2. - 3.

GPU和显示器共用一块buffer,会引起画面撕裂。

drawing without vsync.png

- CPU:表示CPU绘制的时间段 - GPU:表示GPU合成back buffer的时间段 - Display:显示器读取frame buffer的时间段

按时间顺序: - 1.Display显示第0帧画面,而CPU和GPU正在合成第1帧,且在Display显示下一帧之前完成了。 - 2.由于GPU在Display第一个VSync来之前完成了back buffer的填充,此时交换back buffer和frame buffer,屏幕进行刷新,可以正常显示第一帧数据。 - 3.再来看第2个VSync,第二个VSync到来之时,GPU并没有及时的填充back buffer,这个时候不能交互buffer,屏幕刷新的还是第1帧的画面。就说这里发生了“jank” - 4.在第3个VSync信号到来时,第2帧的数据已经写入back buffer,第3帧的数据GPU还没写入,所以这个时候交互buffer显示的是第2帧的数据 - 5.同理,在第4个VSync时,第3帧数据已经处理完毕,交换buffer后显示的是第2帧的数据

这里发生的原因是:在第2帧CPU处理数据的时候太晚了,GPU没有及时将数据写入到buffer中,导致jank的发生。

如果可以把CPU绘制流程提前到每个VSync信号来的时候进行CPU的绘制,那是不是就可以让CPU的计算以及GPU的合成写入buffer的操作有完整的16.6ms。

为了进一步优化性能,谷歌在4.1之后对屏幕绘制与刷新过程引入了Project Butter(),系统在收到VSync信号之后,马上进行CPU的绘制以及GPU的buffer写入。 这样就可以让cpu和gpu有个完整的16.6ms处理过程。最大限度的减少jank的发生。

drawing withvsync.png

引入VSync后,新的问题又出现了:如下图:

with问题.png

由于主线程做了一些相对复杂耗时逻辑,导致CPU和GPU的处理时间超过16.6ms,由于此时back buffer写入的是B帧数据,在交换buffer前不能被覆盖,而frame buffer被Display用来做刷新用,所以在B帧写入back buffer完成到下一个VSync信号到来之前两个buffer都被占用了,CPU无法继续绘制,这段时间就会被空着, 于是又出现了三缓存。

为了进一步优化用户体验,Google在双buffer的基础上又增加了第三个buffer(Graphic Buffer), 如图:

三buffer.png

按时间顺序: - 1.第一个jank是无法避免的,因为第一个B帧处理超时,A帧肯定是会重复的。 - 2.在第一个VSync信号来时,虽然back buffer以及frame buffer都被占用了,CPU此时会启用第三个Graphic Buffer,避免了CPU的空闲状态。

这里可以最大限度避免2中CPU空闲的情况,记住只是最大限度,没有说一定能避免。

那又有人要说了,那就再多开几个不就可以了,是的,buffer越多jank越少,但是你得考虑性价比: 3 buffer已经可以最大限度的避免jank的发生了,再多的buffer起到的作用就微乎其微,反而因为buffer的数量太多,浪费更多内存,得不偿失。 buffer收益比: 不过假想下哪天由于硬件的改进,3 buffer已经满足不了的时候,谷歌又会加4 buffer,5 buffer..这都是可能的事情。

前面我们分析“双buffer时代“说过:谷歌在4.1之后对屏幕绘制与刷新过程引入了Project Butter(黄油工程),系统在收到VSync信号之后,马上进行CPU的绘制以及GPU的buffer写入。这样就可以让cpu和gpu有个完整的16.6ms处理过程。最大限度的减少jank的发生。

那就是这小节要讲解的Choreographer,译为编舞者,多么唯美的词,看来写这个源码的开发者也是个很优雅的绅士。

Choreographer在屏幕绘制中的作用: - 1.注册VSync信号回调 - 2.接收SurfaceFlinger服务回调的onSync事件,SurfaceFlinger服务在接收到硬件发出的定时中断信号VSync后,将信号传递给App,这里App的接收者就是1中注册的回调。 一般SurfaceFlinger服务接收到的中断信号VSync和App收到的VSync回调是有个offsets的。

下面就来看下Choreographer在源码层的工作原理:

首先声明当前使用的是8.0的源码

我们知道一个View在添加到窗口中时,绘制流程会调用到ViewRootImpl的setView()方法,setView方法会调用requestLayout()方法请求绘制,requestLayout方法中会调用scheduleTraversals()方法,那就从scheduleTraversals开始吧。

scheduleTraversals方法主要做了下面事情: - 1.先检查mTraversalScheduled是否已经绘制过,没有绘制过继续走下面流程,并将mTraversalScheduled标志置为true,防止重复绘制 - 2.调用当前Handler的Looper的MessageQueue的postSyncBarrier,设置一个同步屏障。 - 3.使用mChoreographer发送一个Choreographer.CALLBACK_TRAVERSAL的任务。

进入mChoreographer.postCallback方法里面看看:

postCallback最终会调用到postCallbackDelayedInternal方法.

postCallbackDelayedInternal做了下面这些事情:1.将action封装到一个CallBackRecord中并放到mCallbackQueues的callbackType索引处2.如果是立即执行的消息,则直接调用scheduleFrameLocked3.如果是延迟消息,则发送一个MSG_DO_SCHEDULE_CALLBACK的msg

我们来看下MSG_DO_SCHEDULE_CALLBACK的Handler逻辑:

这里mHandler = FrameHandler类对象

MSG_DO_SCHEDULE_CALLBACK的消息类型会走到doScheduleCallback,msg.arg1 = callbackType,

进入doScheduleCallback方法:

判断mCallbackQueues[callbackType]是否有需要任务。有任务则执行scheduleFrameLocked 可以看到postCallbackDelayedInternal最终都是执行scheduleFrameLocked方法

直接看scheduleFrameLocked方法

1.检测mFrameScheduled是否已经为true2.如果开启了VSYNC则调用scheduleVsyncLocked方法,没有开启则发送一个MSG_DO_FRAME的msg给mHandler。Android 4.1之后默认开启了VSYNC,所以直接看USE_VSYNC流程即可3.如果是运行在当前线程的上,当前线程是UI线程。则直接调用scheduleVsyncLocked方法。4.如果没有运行在主线程上, 则发送一个MSG_DO_SCHEDULE_VSYNC的msg给mHandler。根据FrameHandler的源码可以看出最终也是走到scheduleVsyncLocked方法

看scheduleVsyncLocked方法:

回到scheduleVsyncLocked方法:调用了mDisplayEventReceiver.scheduleVsync(),mDisplayEventReceiver是FrameDisplayEventReceiver类对象

进入FrameDisplayEventReceiver类scheduleVsync方法中,子类未实现,在其父类DisplayEventReceiver中实现

scheduleVsync中调用的是nativeScheduleVsync方法进行注册,注册的是一个mReceiverPtr,这是一个native层的对象地址,这个地址是在DisplayEventReceiver构造方法中初始化,调用nativeInit方法返回的,nativeInit方法传入一个this,这个this就是前面的mDisplayEventReceiver对象,所以重新回到前面的mDisplayEventReceiver讲解,

FrameDisplayEventReceiver类实现了onVsync方法,这个方法就是native层在接收到VSync信号后回调的方法。onVsync方法直接发送一个异步消息,执行的任务是自己的run方法。

run中执行doFrame(),进入doFrame看看:

doFrame中: - 1.执行输入事件 - 2.处理动画事件 - 3.处理CALLBACK_TRAVERSAL,三大绘制流程,其实就是前面的mTraversalRunnable事件 - 4.处理CALLBACK_

进入doCallbacks方法:

循环执行callbacks中的记录:

? CallbackRecord的run方法在token = null的情况下执行的是action的run方法 这里再看

传入的token确实为null,且action = mTraversalRunnable,

这里token = FRAME_CALLBACK_TOKEN是在什么情况下呢?

在Choreographer的postFrameCallback方法中:

最终也是执行到postCallbackDelayedInternal方法,不同之处在于,其传入的token是FRAME_CALLBACK_TOKEN

那么这个方法有什么作用呢?计算丢帧。

我们使用下面的方法对丢丢帧30次以上在logcat中打印一个日志。1.创建一个FrameCallback子类

2.在Application启动的时候:调用Choreographer的postFrameCallback方法,并传入一个FrameCallback

对这小节总结: - 1.在Choreographer的构造函数中会创建一个FrameDisplayEventReceiver类对象,这个对象实现了onVSync方法,用于VSync信号回调。 - 2.FrameDisplayEventReceiver这个对象的父类构造方法中会调用nativeInit方法将当前FrameDisplayEventReceiver对象传递给native层,native层返回一个地址mReceiverPtr给上层。 - 3.主线程在scheduleVsync方法中调用nativeScheduleVsync,并传入2中返回的mReceiverPtr,这样就在native层就正式注册了一个FrameDisplayEventReceiver对象。 - 4.native层在GPU的驱使下会定时回调FrameDisplayEventReceiver的onVSync方法,从而实现了:在VSync信号到来时,立即执行doFrame方法 - 5.doFrame方法中会执行输入事件,动画事件,layout/measure/draw流程并提交数据给GPU。这样就闭环了

绘制流程图如下:来自《Android 之 Choreographer 详细分析》

Android 之 Choreographer 详细分析.png

核心思想:在主线程Looper获取msg的时候,让异步消息优先执行,同步消息滞后。

这里先上一张原理图:

同步机制.png

我们依次对图中几个点进行源码讲解: - 1.同步屏障的创建 - 2.异步消息的优先执行

使用的是:MessageQueue#postSyncBarrier()

注释中有说明了,这里的屏障消息就是一个target为null的消息,因为其不需要执行任务消息,只是用来设置一堵墙.

再根据时间戳将其插入到合适的位置。

移步到:MessageQueue的next方法:

优先判断是否有同步屏障存在,然后取同步屏障后面的异步消息进行处理。就达到了优先执行异步消息的目的。

好了,关于同步屏障机制就讲到这里。

有了上面这些基础讲解。下面对一开始的问题进行一个总结归纳。

16.6ms是指刷新频率为是60HZ,1s需要执行60次,平均每次16.6ms。也可以理解为VSync的一个周期是16.6ms。 并非每次16.6ms都会执行三大绘制流程,屏幕静止状态,CPU并不会执行绘制流程

画面撕裂是早期使用的是一个buffer进行屏幕的刷新读取和GPU的写入操作,且不存在同步锁的情况下,新数据覆盖旧的数据导致一张画面显示多帧的场景

主线程耗时,布局层级太多会影响CPU的计算和GPU的合成过程,超过VSync信号后,需要等下一个VSync信号来才能进行buffer交换,发生丢帧现象

丢帧是指在第一个VSync信号来之前并没有合成好back buffer数据,无法交换buffer,屏幕刷新的还是上一帧数据,这就是丢帧。 下一个VSync信号来之后,back buffer绘制好后,再交换buffer,所以其不会丢弃,而是延后显示,直观感受就是卡顿。

双缓冲是指使用两个buffer进行数据的缓存,一个用于GPU的合成,一个用于屏幕的刷新,互不干扰,防止出现画面撕裂的场景 有了双缓存还是会出现丢帧的现象和CPU的空闲等问题。 三缓冲就是在双缓冲上再增加一个Graphic Buffer,避免CPU长时间空闲。

VSync(垂直同步)有两个作用: 1.提醒GPU进行buffer的交换 2.提醒CPU立即进入屏幕绘制过程,别闲着啦。

编舞者Choreographer是4.1以后引入的: 作用:用来提醒CPU在VSync信号来时立即对View进行绘制,防止出现CPU空闲状态。

好了,就讲到这里吧,上面文章涉及了:UI绘制流程,Handler同步异步消息机制,屏幕刷新流程,卡顿优化等知识点

如果你能把本文涉及的知识点都吸收,对后期framework层的学习是很有帮助的。

如果你喜欢我的文章,请帮忙点赞,关注,这是对我的巨大鼓励!

欢迎关注我的公众号:

参考

数据的流动——计算机是如何显示一个像素的

聊聊Android屏幕刷新机制

Android垂直同步信号VSync的产生及传播结构详解

本文经用户投稿或网站收集转载,如有侵权请联系本站。

发表评论

0条回复