Vsync 来龙去脉

Posted Danny_姜

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Vsync 来龙去脉相关的知识,希望对你有一定的参考价值。

假如没有Vsync

之前在 Android 上屏流水账 和 不同视角下的Android上屏 中已经介绍过View的绘制会经过Measure、Layout、Draw这3个阶段,而这3个阶段的工作都是由CPU来负责包完成。另外CPU还会负责一些用户输入、View动画等事件,这部分工作都是在UI线程中完成。

当CPU绘制完成之后,会在RederThread线程中将这部分数据提交给GPU。GPU负责对这些数据进行栅格化(Rasterization)操作,并将数据缓存到Frame Buffer中交给屏幕Display显示。如下图:

正常情况下,完美的屏幕绘制过程应当如下图所示: 

如上图所示,通过双缓冲技术再加上在应用层的优化,大多情况下已经完全能满足程序流程运行。

但是有时还是会发生"屏幕撕裂"或者"丢帧"现象。这是因为屏幕刷新率和GPU绘制帧率并不一定是一致的。

屏幕撕裂

Frame Rate > Screen Refresh Rate

如果Frame Rate(帧率) > Screen Refresh Rate(屏幕刷新率)的情况下,因为GPU渲染的频率高于屏幕刷新的频率,就有可能造成在屏幕进行刷新的过程中,有新的frame buffer数据从GPU同步过来,因此会导致屏幕撕裂现象,如下图:

下图中,可以从代码层面看出发生"屏幕撕裂"的时间点

丢帧

Invalidate & requestLayout

屏幕刷新率是一个硬件指标,当手机出厂设置之后,屏幕刷新率就已经确定,软件层并没有办法对其进行修改。可是软件层触发View绘制的时机是随机的(代码里可以在任意时间调用Invalidate或者requestLayout刷新),因此我们无法控制View绘制的起始时间。比如上图中的绘制过程也有可能会发生以下情况:

可以看出在”帧1"阶段,虽然CPU和GPU所消耗时间小于16ms,但是它们开始的时间太晚,距离下一次屏幕刷新太近。所以当下一次屏幕刷新时,屏幕动frame buffer中拿到的数据还是”帧1"的数据,还是会”丢帧"。

有了Vsync之后

为了解决这个问题,android系统引入了Vsync机制。每隔16ms硬件层发出vsync信号,应用层接收到此信号后会触发UI的渲染流程,同时vsync信号也会触发SurfaceFlinger读取Buffer中的数据,进行合成显示到屏幕上。简单来讲就是将上面CPU和GPU的开始时间与屏幕刷新强行拖拽到同一起跑线。实现下图效果:

可以看出vsync的频率同屏幕刷新频率是一致的,因此View的渲染和SurfaceFlinger的合成也都按照vsync的信号的频率有条不紊的进行着。 

下图中,可以从代码层面看出发生Vsync发生的时间点

Choreographer执行者

那么软件层是如何接受硬件发出的vsync信号,并进行View绘制操作的呢?答案就是Choreographer。

每次调用View的invalidate时,流程都会执行到ViewRootImpl的requestLayout方法,如下:

在requestLayout中调用scheduleTraversals,此方法具体如下:

这个方法最核心的就是红框中所示的mChoreographer.postCallback方法,这个方法将TraversalRunnable以参数的形式传递给Choreographer,方法实现具体如下:

通过一系列调用,最终代码执行到了以下方法

图中红框中代码表示在ViewRootImpl中传入的TraversalRunnable插入到一个CallbackQueue中。后续当Choreographer接收到vsync信号时,会将此TraversalRunnable从队列总取出执行,具体稍后介绍。

何时注册vsync信号

Choreographer是何时注册vsync信号的呢?还是要重新看一下上图中的postCallbackDelayedInternal方法,此方法中在将TraversalRunnabl插入队列CallbackQueue之后,还有一个比较重要的操作—sheduleFrameLocked,方法具体如下:

可以看出,最终调用了一个native的本地方法nativeScheduleVsync,这个方法实际上就是向系统订阅一次vsync信号。android系统每过16.6ms会发送一个vsync信号。但这个信号并不是所有app都能收到的,只有订阅了的才能收到。这样设计的合理之处在于,当UI没有变化的时候就不会去调用nativeScheduleVsync去订阅,也就不会收到vsync信号,减少了不必要的绘制操作。

注意:
每次订阅只能收到一次vsync信号,如果需要收到下次信号需要重新订阅。比如Animation的实现就是在订阅一次信号之后,紧接着再次调用nativeScheduleVsync方法订阅下次vsync信号,因此会不断的刷新UI。

何时接收vsync信号

注册vsync信号的操作是有FrameDisplayEventReceiver中的nativeScheduleVsync方法实现的,而vsync信号实际上也是有FrameDisplayEventReceiver来接收。当它接收到vsync信号后,会调用其内部的onVsync方法,如下所示:

在onVsync方法中,会将FrameDisplayEventReceiver自身发送的MessageQueue中,所以需要查看其run方法,如下:

最终调用了Choreographer的doFrame方法,这是一个非常重要的方法。就是在这个方法中将之前插入到CallbackQueue中的Runnable取出来执行。部分核心代码如下所示:

可以看出在doFrame方法中主要完成2大操作:

  • 图中1处进行掉帧逻辑计算,并添加用于性能分析的Trace日志

  • 图中2处执行各种Callbacks,其实这就是一帧内真正做的事情。可以看出其实Android屏幕绘制一帧主要就是做处理Input、animation、traversal3件事情。

Choreographer小结:

Choreographer是一个承上启下的角色:

  • 承上:接收应用层的各种callback输入,包括input、animation、traversal绘制。但是这些callback并不会被立即执行。而是会缓存在Choreographer中的CallbackQueue中。

  • 启下:内部的FrameDisplayEventReceiver负责接收硬件层发成的vsync信号。当接收到vsync信号之后,会调用onVsync方法 -> doFrame -> doCallbacks,在doCallbacks中从CallbackQueue中取出进行绘制的TraversalRunnable,并调用起run方法进行绘制。

通过这样一套机制,保证软件层和屏幕刷新处于同一频率,使Android app可以以一个比较稳定的帧率运行,减少因为频率波动造成”丢帧”的概率。 

如果你喜欢本文

长按二维码关注

开发者涨薪指南 48位大咖的思考法则、工作方式、逻辑体系

以上是关于Vsync 来龙去脉的主要内容,如果未能解决你的问题,请参考以下文章

Vsync 来龙去脉

Vsync 来龙去脉

Android 显示系统:Vsync机制

wpf 等待 vsync(图片)

理解 VSync

Vsync机制和Choreographer详解