Android 渲染机制
View 的绘制流程有3个步骤,分别是measure 、layout和draw,它们主要运用在系统的应用框架层,而真正将数据渲染到屏幕上的则是系统Native层的SurfaceFlinger服务来完成的。
绘制过程主要由CPU来进行Measure、Layout、Record、Execute的数据计算工作,GPU负责栅格化、渲染。CPU和GPU是通过图形驱动层来进行连接的,图形驱动层维护了一个队列,CPU将display list添加到该队列中,这样GPU就可以从这个队列中取出数据进行绘制。
应用程序的帧率是多少呢?我们的目标是,让应用程序在其整个生命周期中,始终保持60FPS的帧速率。这意味着要在一秒内刷新60次,也就是每16.6667毫秒刷新一次。
Android 系统每隔16ms发出VSYNC信号,触发对UI进行渲染,触发对UI进行渲染,如果每次渲染都成功,这样就能达到流畅的画面所需要的60fps,那么什么事VSYNC呢?VSYNC是Vertical Synchronization(垂直同步)的缩写,是一种定时中断,一旦收到VSYNC信号,CPU就开始处理各帧数据。
屏幕撕裂和垂直同步
关于刷新,主要有两方面需要考虑:
- 帧速率: 指的是设备的GPU每秒能为整个屏幕绘制多少帧。我们的目标是维持Android设备的标准。
- 刷新率: 指的是屏幕在一秒内更新的次数,以赫兹为单位。大部分Android设备的刷新率为60Hz。
刷新率是固定不变的,但帧速率取决于众多因素,其中最重要的因素是开发人员的技能
这两个有可能不同步。当屏幕在刷新过程中,相邻的不同两帧图像被
绘制到同一个屏幕上,造成屏幕出现明显的截断现象。这一现象直到屏幕完成下一次绘制才消失。我们把这种情况称为屏幕撕裂,他会影响GPU系统的每次显示。图片上非来那段的线被称为撕裂点,它们是屏幕撕裂的结果。
造成这一现象的主要原因是,绘制帧的数据源是单向的。新的一帧会覆盖在上一帧,然而只有一个缓冲区可供读取。当屏幕要刷新时,它会从缓冲区读取要被绘制的帧,但如果这一帧还处于未完成的状态,就会出现上图的现象。
针对该问题最常用的解决方案是采用双缓冲区。该方案可通过以下方式实现:
- 所有的绘制操作被保存在一个后台缓冲区。
- 当这些操作完成时,将整个后台缓冲区复制到另一块,被称为前台缓冲区的内存区域。
复制操作与屏幕的速率保持同步。屏幕只需要从前台缓冲区读取,以避免屏幕撕裂,并且所有的后台绘制操作不会影响屏幕的绘制。但是,在讲缓冲区从后台复制到前台的这一过程中,需要有一种机制来防止屏幕被更新,我们把这种机制成为垂直同步(VSYNC)。
垂直同步并不是问题的终极解决方案。如果帧速率大于等于刷新率,那么一切都很正常。如下图,帧速率为80FPS,而刷新率为60Hz。对屏幕绘制来说,新的帧一直够用,所以屏幕不会出现延迟。
但如果帧速率小于刷新率,会发生什么情况呢?接下来,让我们通过示例一步步分析一个40FPS的GPU和一个60Hz刷新率的屏幕在一起会出现什么情况?也就会是说,帧速率是刷新率的2/3,导致每一帧会被屏幕刷新1.5次。
- 屏幕首次刷新,每一帧进入前台缓冲区,同时GPU在后台缓冲区准备第二帧。
- 屏幕第二次刷新,然而此时GPU还未完成绘制操作,刚进行到操作的2/3阶段,因此,不能将它复制到前台缓冲区。
- 屏幕第三次刷新,第二帧已经被复制到前台缓冲区,这时它可以被显示在屏幕上。而GPU也在为第三帧做准备。
- 屏幕第四次刷新,第二帧被绘制在屏幕上,因为它已经在前台缓冲区,同时GPU仍然在为第三帧做准备。
- 屏幕第五次刷新和第二次刷新类似的,第三帧不能被显示在屏幕上,因为需要等待一次新的刷新。所以,新的一帧在第二次刷新中才能被显示。
最终,在四次屏幕刷新中,实际只绘制了两帧。当帧速率小于刷新率时,这种情况每次都会发生。哪怕帧速率为59FPS,而实际显示到屏幕上也仅为每秒30帧。因为GPU在后台缓冲区启动一次新的绘制操作之前,需要等待一次新的刷新发生。
产生卡顿原因有很多,主要有哪些呢
- 布局Layout过于复杂,无法在16ms内完成渲染
- 同一时间动画执行的次数过多,导致CPU或GPU负载过重
- View过度绘制,导致某些像素在同一帧时间内被绘制多次
- 在UI线程中做了稍微耗时的操作
- GC回收时暂停时间过长或者频繁的GC产生大量的暂停时间