iOS Rendering 二: 屏幕成像和卡顿
屏幕成像原理
首先从过去的 CRT
显示器原理说起。CRT 的电子枪按照上面方式,从上到下一行行扫描,扫描完成后显示器就呈现一帧画面,随后电子枪回到初始位置继续下一次扫描。为了把显示器的显示过程和系统的视频控制器进行同步,显示器(或者其他硬件)会用硬件时钟产生一系列的定时信号。当电子枪换到新的一行,准备进行扫描时,显示器会发出一个水平同步信号(horizonal synchronization
),简称HSync
;而当一帧画面绘制完成后,电子枪回复到原位,准备画下一帧前,显示器会发出一个垂直同步信号(vertical synchronization
),简称 VSync
。显示器通常以固定频率进行刷新,这个刷新率就是VSync
信号产生的频率。尽管现在的设备大都是液晶显示屏了,但原理仍然没有变。
-
垂直同步信号(
VSync
):
屏幕发出VSync之后,就表示将要进行新一帧画面的显示,于是开始从帧缓存里面读取经过GPU渲染好的用于显示的数据 -
水平同步信号(
HSync
):
显示器从帧缓存里拿到数据之后,是从上到下一行一行的刷新的,刷新完一行,就发出一个HSync,直到最下面一层显示出来,这样,一帧的画面就完成了显示。
iOS中的渲染过程
在iOS的界面渲染中,也是需要遵循上述的屏幕渲染原理的,这是一系列复杂过程,主要使用了CPU
,GPU
和对应的双缓存机制
-
CPU(Central Processing Unit)
:-
中央处理器
,在iOS程序中,负责对象的创建和销毁、对象属性的调整、布局的计算、文本的计算和排版规格、图片的格式转码和解码、图像的绘制(Core Graphic)
-
-
GPU(Graphics Processing Unit)
:-
图形处理器
,负责纹理的渲染。如果没有接触过OpenGL的朋友,可能不太好理解纹理渲染这个概念,我们知道,屏幕上面的物理元件是像素,我们在屏幕上面看到的图片,文字,视频,就是由屏幕上的所有像素,通过控制色值变化而呈现出来的。那么像素的色值数据,就是由GPU计算得出的,然后将这些数据提交给视频控制器,由它负责显示到屏幕上。 -
比CPU使用更少的电来完成工作并且GPU的浮点计算能力要超出CPU很多。
-
GPU的渲染性能要比CPU高效很多,同时对系统的负载和消耗也更低一些,所以在开发中,我们应该尽量让CPU负责主线程的UI调动,把图形显示相关的工作交给GPU来处理,当涉及到光栅化等一些工作时,CPU也会参与进来
-
-
双缓冲机制
: -
iOS中采用的是双缓冲机制,分为
前帧缓存
和后帧缓存
。 -
GPU会预先渲染好一帧放入一个缓冲区内(前帧缓存),让视频控制器读取,当下一帧渲染好后,GPU会直接把视频控制器的指针指向第二个缓冲器(后帧缓存)
-
当你视频控制器已经读完一帧,准备读下一帧的时候,GPU会等待显示器的VSync信号发出后,前帧缓存和后帧缓存会瞬间切换,后帧缓存会变成新的前帧缓存,同时旧的前帧缓存会变成新的后帧缓存。
卡顿
屏幕撕裂 Screen Tearing
在这种单一缓存的模式
下,最理想的情况就是一个流畅的流水线:每次电子束从头开始新的一帧的扫描时,CPU+GPU
对于该帧的渲染流程已经结束
,渲染好的位图已经放入帧缓冲器中
。但这种完美的情况是非常脆弱的,很容易产生屏幕撕裂:
垂直同步 Vsync + 双缓冲机制 Double Buffering
解决屏幕撕裂、提高显示效率的一个策略就是使用垂直同步信号 Vsync
与双缓冲机制 Double Buffering
。根据苹果的官方文档描述,iOS 设备会始终使用 Vsync + Double Buffering
的策略。
垂直同步信号(
vertical synchronisation,Vsync)相当于给帧缓冲器加锁
:当电子束完成一帧的扫描,将要从头开始扫描时,就会发出一个垂直同步信号。只有当视频控制器接收到 Vsync 之后,才会将帧缓冲器中的位图更新为下一帧,这样就能保证每次显示的都是同一帧的画面,因而避免了屏幕撕裂。
但是这种情况下,视频控制器在接受到 Vsync 之后,就要将下一帧的位图传入,这意味着整个 CPU+GPU 的渲染流程都要在一瞬间完成,这是明显不现实的。所以双缓冲机制会增加一个新的备用缓冲器(back buffer
)。渲染结果会预先保存在 back buffer 中,在接收到 Vsync 信号的时候,视频控制器会将 back buffer 中的内容置换到 frame buffer 中,此时就能保证置换操作几乎在一瞬间完成(实际上是交换了内存地址)。
掉帧 Jank
启用 Vsync 信号以及双缓冲机制之后,能够解决屏幕撕裂的问题,但是会引入新的问题:掉帧。如果在接收到 Vsync 之时 CPU 和 GPU 还没有渲染好新的位图,视频控制器就不会去替换 frame buffer 中的位图。这时屏幕就会重新扫描呈现出上一帧一模一样的画面。相当于两个周期显示了同样的画面,这就是所谓掉帧的情况。
如图所示,A、B 代表两个帧缓冲器,当 B 没有渲染完毕时就接收到了 Vsync 信号,所以屏幕只能再显示相同帧 A,这就发生了第一次的掉帧。
三缓冲 Triple Buffering
事实上上述策略还有优化空间。我们注意到在发生掉帧的时候,CPU 和 GPU 有一段时间处于闲置状态:当 A 的内容正在被扫描显示在屏幕上,而 B 的内容已经被渲染好,此时 CPU 和 GPU 就处于闲置状态。那么如果我们增加一个帧缓冲器,就可以利用这段时间进行下一步的渲染,并将渲染结果暂存于新增的帧缓冲器中。
如图所示,由于增加了新的帧缓冲器,可以一定程度上地利用掉帧的空档期,合理利用 CPU 和 GPU 性能,从而减少掉帧的次数。
屏幕卡顿的本质
手机使用卡顿的直接原因,就是掉帧
。前文也说过,屏幕刷新频率必须要足够高才能流畅。对于 iPhone 手机来说,屏幕最大的刷新频率是 60 FPS,一般只要保证 50 FPS 就已经是较好的体验了。但是如果掉帧过多,导致刷新频率过低,就会造成不流畅的使用体验。
这样看来,可以大概总结一下
屏幕卡顿的根本原因:CPU 和 GPU 渲染流水线耗时过长,导致掉帧
。
Vsync 与双缓冲的意义:强制同步屏幕刷新,以掉帧为代价解决屏幕撕裂问题
。
三缓冲的意义:合理使用 CPU、GPU 渲染性能,减少掉帧次数。