〇、序章
作为一个面试中经常出现的话题,离屏渲染(Offscreen rendering)恐怕是极高频率提起的——最熟悉的陌生人,我们多少知道一点,也了解到由于它的出现,会带来的屏幕刷新卡顿,滑动式不跟手等体验,但是又不是知道的那么透彻。在这篇文章里,我将比较详细的讲解一下其概念、发生的场景,以及解决方案。
0.1 概念
离屏渲染,就是计算机在帧缓冲区外另外开辟一个区域,用来存储多个复杂涂层的组合绘制结果的过程。
说到离屏渲染的概念,先得了解计算机屏幕刷新原理,以及与之对应的屏内渲染。
① 屏内渲染(Inscreen Rendering):
CPU和GPU 可以在其自有的帧缓冲区(Frame Buffer Zone)内进行图像绘制,无需额外开辟空间,待绘制完成后将绘制结果直接进行渲染
② 离屏渲染(Offscreen Rendering):
CPU与GPU 在渲染图像之前,由于工作任务的繁重,需要在当前GPU 的双帧缓冲区之外,额外开辟一个缓冲区用来绘制图像,这个缓冲区被称为离屏缓冲区(Offscreen Buffer zone),待绘制结束之后,系统将缓冲区绘制的结果渲染到帧缓冲区,进行图像展示。
一、CPU 与 GPU
1.1 名词解释
CPU(Central Processing Unit): 是计算机的主要设备之一,功能主要是解释计算机指令以及处理计算机软件中的数据。
GPU(Graphics Processing Unit):又称为显示核心、视觉处理器、显示芯片,是一种专门在个人电脑、工作站、游戏机和一些移动设备上运行绘图运算工作的微处理器。
1.2 组织架构:
1.2.1 CPU 组织架构
CPU 主要有以下组织:
- Control:控制器
- ALU: 算术逻辑单元
- CACHE:缓存区
- DRAM:动态随机存储器
示意图如下:
1.2.2 GPU组织架构
GPU有如下组织构成:
- BIF(Bus Interface):总线接口
- PMU(Power Management Unit): 电源管理单元
- VPU(Video Processing Unit):视频处理单元
- DIF(Display Interface):显示接口
- GMC(Graphic Memory Controller):图像内存控制器
- GCA(Graphic and Compute Array):图形和计算数组
如下:
1.3 特点
类型 | 身份与作用 | 特点 | 特点 |
---|---|---|---|
CPU | 运算核心/控制 | 逻辑复杂,处理数据 | 依赖性非常高,假并发 |
GPU | 绘图运算的微处理器 | 逻辑简单,绘制图形 | 依赖性低,多核真正并发 |
二、计算机渲染原理
2.1 计算机扫描方式
2.1.1 随机扫描显示
随机扫描显示,常见于老的CRT 显示器,通过固定的线段来画出图形的线段,流程是从屏幕中的一个点移动到下一个点。在画完图后,系统会循环回到第一行,重复绘制下一个图形。
该过程如图所示:
优点如下:
- 电子束仅仅指向绘制图形的部分区域屏幕
- 产生的线条是平滑的
- 分辨率较高
缺点:
无法绘制较为复杂的阴影场景
2.1.2 光栅扫描显示
2.1.2.1 概念
光栅扫描效果如下图所示,它是由电极发射电子束从上到下,从左到右依次扫描,形成图形。
2.1.2.2 特点
左上角开始横向扫描
依次纵向扫描
由像素阵列组成
显示整个光栅所需的时间
扫描只与屏幕像素油管,而和图像无关
扫描会产生视觉暂留
- 16帧-保证连贯
2.1.2.3 优点如下:
- 逼真的形象
- 数百万种将生成的颜色
- 阴影场景是可能的。
2.1.2.4 缺点如下:
- 低解析度
- 昂贵
2.1.3 光栅扫描结构
显示器
视频控制器 -
- 负责控制刷新 - 管理帧缓冲区与缓冲器
- 负责将帧缓冲区的内容绘制到显示器
帧缓冲区
- 由阵列组成
- 存储颜色值(黑白值)
- 又名:显存
- 连续的计算机存储空间,存储即将显示的渲染数据
- 计算:大小 60 * 60 的图片 = 60 * 60 * 4 (RGBA) = 14400bit
简单光栅扫描显示系统结构
- 系统总线
- CPU
- 系统内存
- 显示处理器
- 显示处理器存储区
- 帧缓冲区
- 显示控制器
- 显示器
- 系统总线
2.2 计算机渲染流程
三、iOS 环境下渲染原理
3.1 渲染流程
iOS 下的流程如下图所示,总结为一下几个步骤:
- App 配置图形代码。如生成一个 UIImageView 或 CoreAnimation动画的生成
- iOS 框架代码的调用。通常有CoreGraphics、CoreAnimation和CoreImage。
- 系统底层引擎的调用与图形绘制计算。这部分工作交由OpenGL ES 或者原生的Metal 来完成。
- 调用系统显存GPU驱动器。
- 通过GPU 驱动器,将绘制计算好的数据存入GPU 也就是帧缓冲区。
- 通过视频控制器将GPU 内绘制好的数据,按照每帧的方式显示在显示器上面。
3.2 iOS 下的CoreAnimation
3.2.1 结构
UIKit/AppKit为表层,来驱动 CoreAnimation 驱动 Metal/CoreGraphic 来驱动图形硬件
Render, compose, and animate visual elements –Apple
渲染,构建,和将视觉组件构成动画 —— Apple
3.2.2 layer
CALayer
An object that manages image-based content and allows you to perform animations on that content.
用来显示位图,比如有content
属性
UIView
- 绘制和动画
- 布局和子View 的管理
- 点击事件管理
CALayer
- 渲染
- 动画
四、CoreAnimation 渲染
4.1 Application - 应用
- HandleEvents - 处理事件
- Commit Transaction - 提交事件,如(图片解码)
4.2 RenderServer - 渲染服务
主要做以下两组工作:
- Decode - 解码
- Draw Calls - 显示调用(需要等待下一个Runloop)
4.3 GPU 内的操作
- 会将 CoreAnimation 提交OpenGL
- 调用GPU 开启渲染流程
- 顶点数据
- 顶点着色器
- 偏远着色器
4.4 显示
在下一个Runloop,从帧缓冲区提取数据,显示到屏幕
五、画面撕裂卡顿与掉帧
5.1 画面撕裂
定义
在G-SYNC中有一个很重要的关键词,那就是“屏幕刷新率”。对于传统显示屏来说,它的屏幕刷新率是固定的,例如60Hz的刷新率,就是指每秒钟固定刷新60帧图像。如果说显卡输出图像的速度与刷新率刚好吻合,那意味着每一张图像都可以显示在用户面前,形成一个连贯、流畅影像。
但是在现实中显卡输出的图像帧率并不是固定的,它输出一帧图像的时间可能会高于1/60秒,也能会低于1/60秒,这样显卡的输出帧率与屏幕刷新率就不同步了。当输出帧率高于屏幕刷新率的时候,显卡输出的图像就不一定每一帧都能显示在屏幕上,如果画面恰好处于动态变换的过程中,这样的影像就不连贯了,有可能会产生“图像上半部分是前一帧,下半部分是后一帧”的问题,通俗来说就是图像撕裂。
效果图
原因
在60Hz 的扫描率下,在1/60秒周期过后,CPU 交互给GPU 的计算工作未完成,无法给予最新的图片,故帧缓冲区未及时更新,还是上一帧的内容。
苹果解决方案
垂直同步 Vsync (Vertical sync)
- 帧缓冲区加锁
- 扫描未完成,显示图片全是上一帧的图片
- 保证图片的扫描是完整的
双缓冲区
两个缓冲区,一个展示时,另一个在做准备
// 图片
缺点:掉帧
5.2 卡顿与掉帧
5.2.1 掉帧:
原因:当接受到Vsync 垂直信号时,CPU/GPU 的 计算还未完成(准备好),控制器会从帧缓冲区取出并显示上一帧的图像,从而本帧为正确显示,成为掉帧
三缓冲区:
- CPU/GPU运算时,还有个缓冲区可以存储数据
5.2.2 屏幕卡顿
- CPU/GPU 渲染流水线耗时过长,导致掉帧
- Vsync + doubleBuffering 为了解决屏幕撕裂,会付出掉帧的代价
- 三缓冲区时:合理使用CPU/GPU,仅仅是减少了掉帧的次数
六、产生原因
6.1 垂直同步Vsync 与双缓冲区
6.1.1 垂直同步
苹果对移动设备采用的是垂直同步Vsync + 双缓冲区的策略:计算机发出垂直信号,向显存——也就是帧缓冲区(Frame Buffer Zone)索取计CPU和GPU 绘制好的帧图像,从而由视频控制器显示到屏幕中,接下来发出下一次垂直信号。
这个周期由刷新频率而定,一般来说屏幕刷新频率是60Hz,这样每个刷新周期,就是16.666ms(1 /60 * 1000)。
6.2 离屏缓冲区最终由来
- 一旦绘制的图层较多,以及对图层绘制较为复杂(设置圆角、透明度以及阴影等),由于帧缓冲区对绘制的图层使用完毕会之后都会丢弃,就无法满足保存临时结果。
- 为了使得多个图层的绘制结果能再处理过程中得以保留,系统会自动开辟一个缓冲区——离屏缓冲区,用来保存临时生成的结果。
- 当所有的图层运算完毕,系统将礼品缓冲区的绘制结果放入帧缓冲区,由帧缓冲区交付结果,显示到屏幕中。
6.3 渲染举例
6.3.1 代码
我们举例,一个视图上放一张图片视图,代码如下:
- (void)addContentView{ |
运行后,打开模拟器,在Device 中打开 离屏渲染检测按钮—— Color off-screen Rendered ,即
可以得到如下的图片,四周的黄色区域显示——设置过背景和圆角的这张图片,已经参与了离屏渲染了:
原因:两个图层进行了绘制并融合,需要开辟离屏渲染区域。
七、优点、缺点与特性
7.1 优点:
- 可以解决复杂图层渲染
- 复用复杂图形:如果图像多次出现,可以保存在离屏渲染缓冲区,达到复用的优点。
7.2 缺点:
性能问题——掉帧
由于离屏渲染往往是要承担较为复杂的绘制过程,通常是超过1个图层的复杂绘制,当硬件性能低下,以及绘制图层复杂双重压力下,CPU 往往无法在一个扫描周期——也就是16.67ms 内完成绘制工作并交付GPU,为了维护图片的完整性,系统会选用上一帧的图片来显示,虽然只是1帧图像的滞后,肉眼还是能察觉到图片的滞后——尤其是在视图滚动时,更为明显,这就是我们常常提到的掉帧。
7.3 特性
- 自动触发
- 空间有限制:大小为屏幕像素的2.5倍
8.4 离屏渲染的模式
- 拿到第一个图层,结果放入离屏渲染区
- 拿到第二个图层,结果放入离屏渲染区
- 拿到第三个图层,结果放入离屏渲染区
- 从离屏渲染区,获取第一个图层,添加圆角
- 从离屏渲染区,获取第二个图层,添加圆角
- 从离屏渲染区,获取第三个图层,添加圆角
- 3个图层,进行组合,放入帧缓冲区
- 进行显示到屏幕
小结: 多个图层特殊处理,每一步都需要临时的存出结果
八、产生的场景
8.1 圆角
对,就是圆角,可能印象中,设置了圆角就会带来离屏渲染,真的是这样吗?
先看看苹果对圆角(cornerRadius)的定义:
Setting the radius to a value greater than
0.0
causes the layer to begin drawing rounded corners on its background. By default, the corner radius does not apply to the image in the layer’scontents
property; it applies only to the background color and border of the layer. However, setting themasksToBounds
property toYES
causes the content to be clipped to the rounded corners.
简单的翻一下:
当给定radius 超过0.0 会导致图层(layer)开始在其背景中绘制圆角。默认下,圆角半径不会应用到图层的内容属性的图像中;它只会应用到其背景和自身的边界线中。因此,将
maskToBounds
设置成YES
,会导致内容切成圆角。
由此可以得到两个结论
- 设置 cornerRadius 不会渲染该图层的内容图像
- 设置 corner Radius 只会渲染 layer 的边框及背景
- 属性 maskToBounds 设置YES,会切成圆角
8.2 开启了光栅化 shouldRasterize
- 如果layer 不能被复用,则没有必要打开光栅化
- 如果layer 不是静态的,需要被频繁修改,比如处于动画之中,那么开启离屏渲染反而影响效率
- 离屏渲染缓存内容有时间限制,缓存内容100ms 内容如果没有被使用,那么它就会丢弃;无法进行复用了
- 离屏渲染缓存空间有限,超过2.5 倍屏幕像素大小,也会失效,且无法进行复用
8.3 常见的触发场景
- 使用了 mask 的layer layer.mask
- 需要进行裁剪的layer —— layer.maskToBound / view.clipToBounds
- 设置了组透明为YES, 且透明度部位1 的layer —— layer.allowsGroupOpacity/layer.opacity
- 添加了投影的layer —— layer.shadow
- 采用了光栅化的 layer —— layer.shouldRasterize
- 绘制了文字的 layer —— UILabel 、CATextLayer、 CoreText等
九、解决方案
10.1 方案一:单一图片圆角
设置切歌边角,以及圆角半径
imageView.clipToBounds = YES; |
9.3 方案二:将圆角图片绘制成位图(YYImage)
这个方案在YYImaage 中已经做得非常好了,主要的原理是,根据传入的圆角,CPU进行上下文重绘制,得到UIImage 图片,由于最终得到的图像只有一张图片,自然不会离屏渲染了,相关代码如下:
- (UIImage *)imageByRoundCornerRadius:(CGFloat)radius |
10.3 方案三: 在现有图层上方遮罩图片
|
小结
从上文得知,离屏渲染是苹果为较为复杂的图像效果做出的一种妥协方案,作为一种空间换时间的方案,必然会损失一些性能,为了挽回这些损失,我们只有在深入理解背后的原理后,方能做到较为理想的结果。