主要介绍 VSYNC、三重缓存

Posted by Allen Vork on October 15, 2018

Synopsis

Google 在 Android4.1 提出了 Project Butter 用于提升系统流畅度。Project Butter 对 Android Display 系统进行了重构,引入了 VSYNC(垂直同步)、Triple Buffer(三重缓存) 和 Choreographer。

Problems

在一个典型的显示系统中,一般包括 CPU、GPU、display 这3个部分。CPU 用于计算数据然后交给 GPU 进行渲染,渲染好后放到 buffer 中存起来,然后 display 会将 buffer 里的数据显示到屏幕上。但显示后会出现2种问题:

tearing

即撕裂。当 CPU/GPU 将数据准备好存入 buffer 中,但 display 还没来得及显示,这时 CPU/GPU 把下一帧的数据往 buffer 中写,还没写完的时候,display 开始读取 buffer 来显示(也就是绘图速度大于显示速度)。这时就会出现显示的上半部分是下一帧的数据,下半部分为上一帧的数据,就是所说的撕裂。

jank

绘图速度过慢的时候,同一帧在屏幕上至少出现2次。

Solutions

1.tearing

撕裂的原因是 display 还没来得及读 buffer 就被重写了,那么就可以准备2个 buffer 即双缓冲。back buffer 用于 CPU/GPU 后台绘制,frame buffer 用于显示。back buffer 准备好后才可以交换,这样就可以避免撕裂问题。但是此时屏幕还没有完整显示上一帧的内容时是不能交换的。那么只有等屏幕处理完成当前帧才能进行交换操作。当扫描完一屏后,会回到第一行进入下一次的循环,中间会有一段空隙(VBI),这个空隙为缓冲区交换的最佳时间。VSYNC 就是利用这个空隙出现的垂直刷新脉冲来保证双缓冲的最佳时间点。

2.jank

我们来看下在双缓冲下,没有 VSYNC 的情况: Display 为显示屏, VSYNC 仅仅指双缓冲的交换。我们来看下将会发生的异常:

  • Step1:Display 显示第0帧,此时 CPU/GPU 渲染第1帧画面,并且在 Display 显示下一帧前完成。
  • Step2:Display 正常渲染第一帧。
  • Step3:出于某种原因,如 CPU 资源被占用,系统没有及时处理第2帧数据,当 Display 显示下一帧时,由于数据没处理完,所以依然显示第1帧,即发生“Jank”。
    上图出现的情况就是第2帧没有在显示前及时处理,导致屏幕多显示第一帧一次,导致后面的帧都延时了。那么如何让第2帧及时绘制呢?
    可以看出,当且仅当 VSYNC 出现时,CPU 就会立即处理下一帧数据,大大降低了 Jank 的概率。而且也杜绝了 CPU/GPU 不停的绘制,导致帧生成速度高于屏幕刷新速度,生成的帧不能显示而被丢弃,这样导致的丢帧情况。引入 VSYNC 后,绘制速度和屏幕刷新速度保持一致了。现在 Android 设备的屏幕刷新频率为 60HZ,那么 CPU/GPU 渲染的时间需要在 16ms 内。当 CPU/GPU 的 FPS 高于 60 HZ 显示效果会很完美,如果设备硬件性能较差,无法达到这个要求会出现什么情况呢? 我们先来看下正常情况,A 和 B 分别代表2个缓冲区。整个过程很顺滑。现在来看下FPS低于屏幕刷新率的情况: 可以看出当第1个 VSYNC 到来时 GPU 还在处理数据,这时 B 缓冲区被占用了,那么就无法进行交换,屏幕依然显示 A 缓冲区的数据。下一个信号到来时,此时 GPU 已经处理完了,那么就可以交换缓冲区,此时屏幕显示 B 缓冲区,CPU/GPU 开始操作 A。下一个信号到来时,A 被占用,那么屏幕依然显示 B 的数据。这种情况就是因为 GPU/CPU 无法在 16ms 内处理完数据而导致缓冲区交换延迟。
    那么有没有办法避免呢?
    因为设备不能升级硬件,我们无法改变 CPU/GPU 渲染的时间,那么第一次 Jank 是无法避免的。我们重点关注 CPU 第一次和第二次执行中间浪费的时间。当第1次信号到来时,由于 GPU 占用了 B,导致屏幕会一直占用 A。两个缓冲区都被占用了,即使此时 CPU 是空闲的,它也没有办法处理下一帧的数据。如果增加一个 buffer,会不会有所改善? 当第一个信号到来时,A、B 都被占用,此时 CPU 开始使用 C 缓冲区来处理下一帧数据。之前第二次发生的 Jank 就避免了。有效的降低了显示错误的几率。可以看出双缓冲和三重缓冲都会有 lag(延时)问题。C 缓冲区延时了16ms才显示。

渲染过程CPU和GPU的分工

我们知道,渲染的过程需要2个核心组件:CPU 和 GPU。

  • CPU:负责 Measure、Layout、Record、Execute 的计算操作。
  • GPU:负责 Rasterization(栅格化)操作

CPU 负责把 UI组件计算成 Polygons(多边形)和 Texture(纹理),然后交给 GPU 进行栅格化。 GPU 的栅格化过程是绘制 Button、Shape、Path、String、Bitmap 等组件最基础的操作。它将这些组件拆分到不同的像素中显示,这是一个耗时操作,GPU 的引入就是为了加快栅格化。
每次从个CPU 转移到 GPU 是一件很麻烦的事,所幸的是 OpenGL ES 可以将需要渲染的纹理 hold 在 GPU Memory 中,在下次需要渲染的时候直接进行操作。

References