离屏渲染-超详细解析


〇、序章

作为一个面试中经常出现的话题,离屏渲染(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): 是计算机的主要设备之一,功能主要是解释计算机指令以及处理计算机软件中的数据。

GPUGraphics Processing Unit):又称为显示核心、视觉处理器、显示芯片,是一种专门在个人电脑、工作站、游戏机和一些移动设备上运行绘图运算工作的微处理器。

1.2 组织架构:

1.2.1 CPU 组织架构

CPU 主要有以下组织:

  • Control:控制器
  • ALU: 算术逻辑单元
  • CACHE:缓存区
  • DRAM:动态随机存储器

示意图如下:

CPU 输入输出逻辑结构

CPU 的组织架构

1.2.2 GPU组织架构

GPU有如下组织构成:

  1. BIF(Bus Interface):总线接口
  2. PMU(Power Management Unit): 电源管理单元
  3. VPU(Video Processing Unit):视频处理单元
  4. DIF(Display Interface):显示接口
  5. GMC(Graphic Memory Controller):图像内存控制器
  6. GCA(Graphic and Compute Array):图形和计算数组

如下:

GPU 组织架构

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 优点如下:
  1. 逼真的形象
  2. 数百万种将生成的颜色
  3. 阴影场景是可能的。
2.1.2.4 缺点如下:
  1. 低解析度
  2. 昂贵

2.1.3 光栅扫描结构

  • 显示器

  • 视频控制器 -

    • 负责控制刷新 - 管理帧缓冲区与缓冲器
    • 负责将帧缓冲区的内容绘制到显示器
  • 帧缓冲区

    • 由阵列组成
    • 存储颜色值(黑白值)
    • 又名:显存
    • 连续的计算机存储空间,存储即将显示的渲染数据
    • 计算:大小 60 * 60 的图片 = 60 * 60 * 4 (RGBA) = 14400bit
  • 简单光栅扫描显示系统结构

    • 系统总线
      • CPU
      • 系统内存
      • 显示处理器
        • 显示处理器存储区
        • 帧缓冲区
    • 显示控制器
    • 显示器

2.2 计算机渲染流程

计算机渲染流程

三、iOS 环境下渲染原理

3.1 渲染流程

iOS 下的流程如下图所示,总结为一下几个步骤:

  1. App 配置图形代码。如生成一个 UIImageView 或 CoreAnimation动画的生成
  2. iOS 框架代码的调用。通常有CoreGraphics、CoreAnimation和CoreImage。
  3. 系统底层引擎的调用与图形绘制计算。这部分工作交由OpenGL ES 或者原生的Metal 来完成。
  4. 调用系统显存GPU驱动器。
  5. 通过GPU 驱动器,将绘制计算好的数据存入GPU 也就是帧缓冲区。
  6. 通过视频控制器将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秒,这样显卡的输出帧率与屏幕刷新率就不同步了。当输出帧率高于屏幕刷新率的时候,显卡输出的图像就不一定每一帧都能显示在屏幕上,如果画面恰好处于动态变换的过程中,这样的影像就不连贯了,有可能会产生“图像上半部分是前一帧,下半部分是后一帧”的问题,通俗来说就是图像撕裂。

  • 效果图

    分裂成3段的图片

  • 原因

    在60Hz 的扫描率下,在1/60秒周期过后,CPU 交互给GPU 的计算工作未完成,无法给予最新的图片,故帧缓冲区未及时更新,还是上一帧的内容。

  • 苹果解决方案

    1. 垂直同步 Vsync (Vertical sync)

      • 帧缓冲区加锁
      • 扫描未完成,显示图片全是上一帧的图片
      • 保证图片的扫描是完整的
    2. 双缓冲区

      两个缓冲区,一个展示时,另一个在做准备

      // 图片

    3. 缺点:掉帧

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)。

iOS正常的渲染流程

6.2 离屏缓冲区最终由来

  1. 一旦绘制的图层较多,以及对图层绘制较为复杂(设置圆角、透明度以及阴影等),由于帧缓冲区对绘制的图层使用完毕会之后都会丢弃,就无法满足保存临时结果。
  2. 为了使得多个图层的绘制结果能再处理过程中得以保留,系统会自动开辟一个缓冲区——离屏缓冲区,用来保存临时生成的结果。
  3. 当所有的图层运算完毕,系统将礼品缓冲区的绘制结果放入帧缓冲区,由帧缓冲区交付结果,显示到屏幕中。

iOS离屏渲染下的渲染流程

6.3 渲染举例

6.3.1 代码

我们举例,一个视图上放一张图片视图,代码如下:

- (void)addContentView{
UIView *contentView = [[UIView alloc] initWithFrame:CGRectMake(50, 50, 300, 300)];
contentView.backgroundColor = [UIColor redColor];
contentView.layer.cornerRadius = 20;
contentView.layer.masksToBounds = YES;
[self.view addSubview:contentView];

UIImageView *iv = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"saga.jpg"]];
iv.backgroundColor = [UIColor blueColor];
iv.layer.cornerRadius = 20;
iv.layer.masksToBounds = YES;
iv.frame = CGRectMake(30, 30, 200, 150);
[contentView addSubview:iv];
}

运行后,打开模拟器,在Device 中打开 离屏渲染检测按钮—— Color off-screen Rendered ,即

可以得到如下的图片,四周的黄色区域显示——设置过背景和圆角的这张图片,已经参与了离屏渲染了:

图层示意图

原因:两个图层进行了绘制并融合,需要开辟离屏渲染区域。

七、优点、缺点与特性

7.1 优点:

  • 可以解决复杂图层渲染
  • 复用复杂图形:如果图像多次出现,可以保存在离屏渲染缓冲区,达到复用的优点。

7.2 缺点:

  • 性能问题——掉帧

    由于离屏渲染往往是要承担较为复杂的绘制过程,通常是超过1个图层的复杂绘制,当硬件性能低下,以及绘制图层复杂双重压力下,CPU 往往无法在一个扫描周期——也就是16.67ms 内完成绘制工作并交付GPU,为了维护图片的完整性,系统会选用上一帧的图片来显示,虽然只是1帧图像的滞后,肉眼还是能察觉到图片的滞后——尤其是在视图滚动时,更为明显,这就是我们常常提到的掉帧。

7.3 特性

  • 自动触发
  • 空间有限制:大小为屏幕像素的2.5倍

8.4 离屏渲染的模式

  1. 拿到第一个图层,结果放入离屏渲染区
  2. 拿到第二个图层,结果放入离屏渲染区
  3. 拿到第三个图层,结果放入离屏渲染区
  4. 从离屏渲染区,获取第一个图层,添加圆角
  5. 从离屏渲染区,获取第二个图层,添加圆角
  6. 从离屏渲染区,获取第三个图层,添加圆角
  7. 3个图层,进行组合,放入帧缓冲区
  8. 进行显示到屏幕

小结: 多个图层特殊处理,每一步都需要临时的存出结果

八、产生的场景

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’s contents property; it applies only to the background color and border of the layer. However, setting the masksToBounds property to YES 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;
imageView.layer.cornerRadius = 10;

9.3 方案二:将圆角图片绘制成位图(YYImage)

这个方案在YYImaage 中已经做得非常好了,主要的原理是,根据传入的圆角,CPU进行上下文重绘制,得到UIImage 图片,由于最终得到的图像只有一张图片,自然不会离屏渲染了,相关代码如下:

- (UIImage *)imageByRoundCornerRadius:(CGFloat)radius
corners:(UIRectCorner)corners
borderWidth:(CGFloat)borderWidth
borderColor:(UIColor *)borderColor
borderLineJoin:(CGLineJoin)borderLineJoin {

if (corners != UIRectCornerAllCorners) {
UIRectCorner tmp = 0;
if (corners & UIRectCornerTopLeft) tmp |= UIRectCornerBottomLeft;
if (corners & UIRectCornerTopRight) tmp |= UIRectCornerBottomRight;
if (corners & UIRectCornerBottomLeft) tmp |= UIRectCornerTopLeft;
if (corners & UIRectCornerBottomRight) tmp |= UIRectCornerTopRight;
corners = tmp;
}

UIGraphicsBeginImageContextWithOptions(self.size, NO, self.scale);
CGContextRef context = UIGraphicsGetCurrentContext();
CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height);
CGContextScaleCTM(context, 1, -1);
CGContextTranslateCTM(context, 0, -rect.size.height);

CGFloat minSize = MIN(self.size.width, self.size.height);
if (borderWidth < minSize / 2) {
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(rect, borderWidth, borderWidth) byRoundingCorners:corners cornerRadii:CGSizeMake(radius, borderWidth)];
[path closePath];

CGContextSaveGState(context);
[path addClip];
CGContextDrawImage(context, rect, self.CGImage);
CGContextRestoreGState(context);
}

if (borderColor && borderWidth < minSize / 2 && borderWidth > 0) {
CGFloat strokeInset = (floor(borderWidth * self.scale) + 0.5) / self.scale;
CGRect strokeRect = CGRectInset(rect, strokeInset, strokeInset);
CGFloat strokeRadius = radius > self.scale / 2 ? radius - self.scale / 2 : 0;
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:strokeRect byRoundingCorners:corners cornerRadii:CGSizeMake(strokeRadius, borderWidth)];
[path closePath];

path.lineWidth = borderWidth;
path.lineJoinStyle = borderLineJoin;
[borderColor setStroke];
[path stroke];
}

UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}

10.3 方案三: 在现有图层上方遮罩图片


@interface RoundImageView()
@property (strong, nonatomic) UIImageView *maskView;
@end

@implementation RoundImageView

- (instancetype)init{
if (self = [super init]) {
_maskView = [[UIImageView alloc] initWithFrame:CGRectZero];
_maskView.image = [UIImage imageNamed:@"maskIV"];
[self addSubview:_maskView];
}
return self;
}

- (void)layoutSubviews{
[super layoutSubviews];
CGRect bounds = self.bounds;
_maskView.frame = bounds;
}

@end

小结

从上文得知,离屏渲染是苹果为较为复杂的图像效果做出的一种妥协方案,作为一种空间换时间的方案,必然会损失一些性能,为了挽回这些损失,我们只有在深入理解背后的原理后,方能做到较为理想的结果。


文章作者: 李佳
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 李佳 !
评论
 上一篇
计算机视觉【01-搭建OpenGL开发环境】 计算机视觉【01-搭建OpenGL开发环境】
一、搭建环境1.1 环境准备: 开发环境:Mac OS、Xcode 11+ OpenGL 工具包(会在本文末尾提供) include 文件夹 libGLTools.a 静态文件 1.2创建项目注意,我们使用Mac 应用来熟悉OpenG
2020-07-14 李佳
下一篇 
计算机视觉【02-OpenGL概念篇】 计算机视觉【02-OpenGL概念篇】
一、计算机视觉简介1.1 概念:计算机视觉(Computer Vision)是一门跨学科科学,用于处理计算机如何从数码相片或视频中获取高层次理解。从工程学的角度来讲,它的目的是理解并将人类视觉系统的工作任务自动化。 计算机视觉(Compu
2020-07-06 李佳
  目录