Android VSYNC与图形系统中的撕裂、双缓冲、三缓冲浅析

先接触两个图形概念: 帧率(Frame Rate,单位FPS)–GPU显卡生成帧的速率,也可以认为是数据处理的速度), 屏幕刷新频率 (Refresh Rate单位赫兹/HZ):是指硬件设备刷新屏幕的频率。屏幕刷新率一般是固定的,比如60Hz的每16ms就刷一次屏幕,可以类比一下黑白电视的电子扫描枪,每16ms电子枪从上到下从左到右一行一行逐渐把图片绘制出来,如果GPU显卡性能非常强悍,帧率可以非常高,甚至会高于屏幕刷新频率。

本文参考视频 Google IO

单缓存画面撕裂与(垂直同步+双缓冲)

什么是画面撕裂?如下:用两帧的部分数据合成一帧。

image.png

The display (LCD, AMOLED, whatever) gets each frame from the graphics chip, and starts drawing it line by line. Ideally, you want the display to get a new frame from the graphics chip after it is finished drawing the previous frame. Tearing occurs when the graphics chip loads a new frame in the middle of the LCD draw, so you get half of one frame and half of another.

如果只有一块缓存,在没有加锁的情况下,容易出现。即:在屏幕更新的时候,如果显卡输出帧率很高,在A帧的数据上半部分刚更新完时,B帧就到了,如果没采取同步锁机制,可以认为帧到了就可用,在继续刷新下半部分时,由于只有一块存储,A被B覆盖,绘制用的数据就是B帧,此时就会出现上半部分是A下半部分是B,这就是屏幕撕裂,个人觉得描述成显卡瞬时帧率过高也许更好。同正常帧绘制相比,正常的帧给时间才就能完整绘制一帧,但撕裂的帧没有机会补全。

image.png

相比较画面撕裂场景如下:

image.png

不过按照Android官方指导的说法,屏幕撕裂还有另外一种解释,那就是显示器用了半成品的帧,不过我是不太理解他说的这点。参考视频

以上说的是只有一块显示存储的情况,其实只要加锁就能解决。那么如果多增加一块显示存储区能解决吗?显卡绘制成功后,先写入BackBuffer,不影响当前正在展示的FrameBuffer,这就是双缓冲,但是理论上其实也不行,因为BackBuffer毕竟也是要展示的,也要”拷贝“到FrameBuffer,在A帧没画完,BackBuffer如果不加干预,直接”拷贝“到FrameBuffer同样出现撕裂。所以同步锁的机制才是关键,必须有这么一个机制告诉GPU显卡,要等待当前帧绘完整,才能替换当前帧。但如果仅仅单缓存加锁的话GPU显卡会被挂啊?这就让效率低了,那就一边加同步锁,同时再多加一个缓存,垂直同步(VSYNC)就可看做是这么个东西,其实两者是配合使用的。

image.png

再来看下VSYNC,屏幕刷新从左到右水平扫描(Horizontal Scanning),从上到下垂直扫描Vertical Scanning,垂直扫描完成则整个屏幕刷新完毕,这便是告诉外界可以绘制下一帧的时机,在这里发出VSync信号,通知GPU给FrameBuffer传数据,完成后,屏幕便可以开始刷新,所以或许称之为帧同步更合适。VSYNC强制帧率和显示器刷新频率同步,如果当前帧没绘制完,即使下一帧准备好了,也禁止使用下一帧,直到显示器绘制完当前帧,等下次刷新的时候,才会用下一帧。比如:如果显示器的刷新频率是60HZ显示器,开了垂直同步后,显示帧率就会被锁60,即使显卡输出高,也没用。对Android系统而言,垂直同步信号除了强制帧率和显示器刷新频率同步外,还有其他很多作用,VSYNC是APP端重绘、SurfaceFlinger图层合成的触发点,只有收到VSYNC信号,它们才会工作,以上便是个人对引入VSYNC与双缓冲的见解。

双缓冲的进阶:三缓冲

在Android系统里,除了双缓冲,还有个三缓冲,不过这个三缓冲是对于屏幕硬件刷新之外而言,它关注的是整个Android图形系统的消费者模型,跟Android自身的VSYNC用法有关系,在 Jelly Bean 中Android扩大了VSYNC使用场景与效果,不仅用在屏幕刷新防撕裂,同时也用在APP端绘制及SurfaceFlinger合成那,此时对VSYNC利用有点像Pipeline流水线,贯穿整个绘制流程,对比下VSYNC扩展使用的区别:

image.png

如果想要达到60FPS的流畅度,每16毫秒必须刷新一帧,否则动画、视频就没那么丝滑,扩展后:

image.png

对于没采用VSYNC做调度的系统来说,比如Project Butter之前的系统(4.1以下),CPU的对于显示帧的处理是凌乱的,优先级也没有保障,处理完一帧后,CPU可能并不会及时处理下一帧,可能会优先处理其他消息,等到它开始处理UI生成帧的时候,可能已经处于VSYNC的中间,这样就很容易跨两个VYSNC信号,导致掉帧。在Jelly Bean中,下一帧的处理被限定在VSync信号到达时,并且看Android的处理UI重绘消息的优先级是比较高的,其他的同步消息均不会执行,从而保证每16ms处理一帧有序进行,同时由于是在每个VSYNC信号到达时就处理帧,可以尽量避免跨越两帧的情况出现

上面的流程中,Android已经采用了双缓冲,**双缓冲不仅仅是两份存储,它是一个概念,双缓冲是一条链路,不是某一个环节,是整个系统采用的一个机制,需要各个环节的支持,从APP到SurfaceFlinger、到图像显示都要参与协作。**对于APP端而言,每个Window都是一个双缓冲的模型,一个Window对应一个Surface,而每个Surface里至少映射两个存储区,一个给图层合成显示用,一个给APP端图形处理,这便是应于上层的双缓冲。Android4.0之后基本都是默认硬件加速,CPU跟GPU都是并发处理任务的,CPU处理完之后就完工,等下一个VSYNC到来就可以进行下一轮操作。也就是CPU、GPU、显示都会用到Buffer,VSYNC+双缓冲在理想情况下是没有问题的,但如果某个环节出现问题,那就不一样了如下(帧耗时超过16ms):

双缓冲jank

可以看到在第二个阶段,存在CPU资源浪费,为什么呢?双缓冲Surface只会提供两个Buffer,一个Buffer被DisPlay占用(SurfaceFlinger用完后不会释放当前的Buffer,只会释放旧的Buffer,直观的想一下,如果新Buffer生成受阻,那么肯定要保留一个备份给SF用,才能不阻碍合成显示,就必定要一直占用一个Buffer,新的Buffer来了才释放老的),另一个被GPU处理占用,所以,CPU就无法获取到Buffer处理当前UI,在Jank的阶段空空等待。一般出现这种场景都是连续的:比如复杂视觉效果每一帧可能需要20ms(CPU 8ms +GPU 12ms),GPU可能会一直超负荷,CPU跟GPU一直抢Buffer,这样带来的问题就是滚雪球似的掉帧,一直浪费,完全没有利用CPU与GPU并行处理的效率,成了串行处理,如下所示

image.png

如何处理呢?让多增加一个Buffer给CPU用,让它提前忙起来,这样就能做到三方都有Buffer可用,CPU跟GPU不用争一个Buffer,真正实现并行处理。如下:

image.png

如上图所示,虽然即使每帧需要20ms(CPU 8ms +GPU 12ms),但是由于多加了一个Buffer,实现了CPU跟GPU并行,便可以做到了只在开始掉一帧,后续却不掉帧,双缓冲充分利用16ms做到低延时,三缓冲保障了其稳定性,为什么4缓冲没必要呢?因为三个既可保证并行,四个徒增资源浪费。

双缓冲保证低延时,三缓冲保证稳定性,双缓冲不在16ms中间开始,有足够时间绘制 三缓冲增加其韧性。

总结

  • 同步是防止画面撕裂的关键,VSYNC同步能防止画面撕裂
  • VSYNC+双缓冲在Android中能有序规划渲染流程,降低延时
  • Android已经采用了双缓冲,双缓冲不仅仅是两份存储,它是一个概念,双缓冲是一条链路,不是某一个环节,是整个系统采用的一个机制,需要各个环节的支持,从APP到SurfaceFlinger、到图像显示都要参与协作
  • 三缓冲在UI复杂情况下能保证画面的连续性,提高柔韧性

作者:看书的小蜗牛

Android VSYNC与图形系统中的双缓冲、三缓冲浅析

仅供参考,欢迎指正

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页