关于图形渲染以及离屏渲染

1. CPU 与 GPU

CPU与GPU的不同

  1. 设计目标的不同,它们分别针对了两种不同的应用场景。
  • CPU需要很强的通用性来处理各种不同的数据类型,同时又要逻辑判断又会引入大量的分支跳转和中断的处理。这些都使得CPU的内部结构异常复杂。而GPU面对的则是类型高度统一的、相互无依赖的大规模数据和不需要被打断的纯净的计算环境。

于是CPU和GPU就呈现出非常不同的架构(示意图):


cpu+gpu.png

图片来自nVidia CUDA文档。其中绿色的是计算单元,橙红色的是存储单元,橙黄色的是控制单元。

  • GPU采用了数量众多的计算单元和超长的流水线,但只有非常简单的控制逻辑并省去了Cache。而CPU不仅被Cache占据了大量空间,而且还有有复杂的控制逻辑和诸多优化电路,相比之下计算能力只是CPU很小的一部分
cpu+gpu-cache.png

从上图可以看出:

Cache, local memory: CPU > GPU

Threads(线程数): GPU > CPU

Registers: GPU > CPU 多寄存器可以支持非常多的Thread,thread需要用到register,thread数目大,register也必须得跟着很大才行。

SIMD Unit(单指令多数据流,以同步方式,在同一时间内执行同一条指令): GPU > CPU。

  • CPU 基于低延时的设cpu,下图:


    cpu.png

CPU有强大的ALU(算术运算单元),它可以在很少的时钟周期内完成算术计算。

当今的CPU可以达到64bit 双精度。执行双精度浮点源算的加法和乘法只需要1~3个时钟周期。

CPU的时钟周期的频率是非常高的,达到1.532~3gigahertz(千兆HZ, 10的9次方).

大的缓存也可以降低延时。保存很多的数据放在缓存里面,当需要访问的这些数据,只要在之前访问过的,如今直接在缓存里面取即可。
复杂的逻辑控制单元。当程序含有多个分支的时候,它通过提供分支预测的能力来降低延时。

数据转发。 当一些指令依赖前面的指令结果时,数据转发的逻辑控制单元决定这些指令在pipeline中的位置并且尽可能快的转发一个指令的结果给后续的指令。这些动作需要很多的对比电路单元和转发电路单元。

  • GPU是基于大的吞吐量设计。


    gpu.png

GPU的特点是有很多的ALU和很少的cache. 缓存的目的不是保存后面需要访问的数据的,这点和CPU不同,而是为thread提高服务的。如果有很多线程需要访问同一个相同的数据,缓存会合并这些访问,然后再去访问dram(因为需要访问的数据保存在dram中而不是cache里面),获取数据后cache会转发这个数据给对应的线程,这个时候是数据转发的角色。但是由于需要访问dram,自然会带来延时的问题。

GPU的控制单元(左边黄色区域块)可以把多个的访问合并成少的访问。

GPU的虽然有dram延时,却有非常多的ALU和非常多的thread. 为了平衡内存延时的问题,我们可以充分利用多的ALU的特性达到一个非常大的吞吐量的效果。尽可能多的分配多的Threads.通常来看GPU ALU会有非常重的pipeline就是因为这样。

所以与CPU擅长逻辑控制,串行的运算。和通用类型数据运算不同,GPU擅长的是大规模并发计算,这也正是密码破解等所需要的。所以GPU除了图像处理,也越来越多的参与到计算当中来。

GPU的工作大部分就是这样,计算量大,但没什么技术含量,而且要重复很多很多次。就像你有个工作需要算几亿次一百以内加减乘除一样,最好的办法就是雇上几十个小学生一起算,一人算一部分,反正这些计算也没什么技术含量,纯粹体力活而已。而CPU就像老教授,积分微分都会算,就是工资高,一个老教授资顶二十个小学生,你要是富士康你雇哪个?GPU就是这样,用很多简单的计算单元去完成大量的计算任务,纯粹的人海战术。这种策略基于一个前提,就是小学生A和小学生B的工作没有什么依赖性,是互相独立的。很多涉及到大量计算的问题基本都有这种特性,比如你说的破解密码,挖矿和很多图形学的计算。这些计算可以分解为多个相同的简单小任务,每个任务就可以分给一个小学生去做。但还有一些任务涉及到“流”的问题。比如你去相亲,双方看着顺眼才能继续发展。总不能你这边还没见面呢,那边找人把证都给领了。这种比较复杂的问题都是CPU来做的。

总而言之,CPU和GPU因为最初用来处理的任务就不同,所以设计上有不小的区别。而某些任务和GPU最初用来解决的问题比较相似,所以用GPU来算了。GPU的运算速度取决于雇了多少小学生,CPU的运算速度取决于请了多么厉害的教授。教授处理复杂任务的能力是碾压小学生的,但是对于没那么复杂的任务,还是顶不住人多。当然现在的GPU也能做一些稍微复杂的工作了,相当于升级成初中生高中生的水平。但还需要CPU来把数据喂到嘴边才能开始干活,究竟还是靠CPU来管的。

贴出CPU和GPU 的官方概念:

  • CPU(Central Processing Unit):现代计算机的三大核心部分之一,作为整个系统的运算和控制单元。CPU 内部的流水线结构使其拥有一定程度的并行计算能力。
  • GPU(Graphics Processing Unit):一种可进行绘图运算工作的专用微处理器。GPU 能够生成 2D/3D 的图形图像和视频,从而能够支持基于窗口的操作系统、图形用户界面、视频游戏、可视化图像应用和视频播放。GPU 具有非常强的并行计算能力。

2.计算机的渲染原理

像素是如何绘制在屏幕上的?

  • 计算机将存储在内存中的形状转换成实际绘制在屏幕上的对应的过程称为 渲染 。渲染过程中最常用的技术就是 光栅化

关于光栅化的概念,以下图为例,假如有一道绿光与存储在内存中的一堆三角形中的某一个在三维空间坐标中存在相交的关系。那么这些处于相交位置的像素都会被绘制到屏幕上。当然这些三角形在三维空间中的前后关系也会以遮挡或部分遮挡的形式在屏幕上呈现出来。一句话总结:光栅化就是将数据转化成可见像素的过程

image.png

GPU 则是执行转换过程的硬件部件。由于这个过程涉及到屏幕上的每一个像素,所以 GPU 被设计成了一个高度并行化的硬件部件。

GPU 图形渲染流水线

GPU 图形渲染流水线的主要工作可以被划分为两个部分:

  • 把 3D 坐标转换为 2D 坐标
  • 把 2D 坐标转变为实际的有颜色的像素

GPU 图形渲染流水线的具体实现可分为六个阶段,如下图所示。

  • 顶点着色器(Vertex Shader)
  • 形状装配(Shape Assembly),又称 图元装配
  • 几何着色器(Geometry Shader)
  • 光栅化(Rasterization)
  • 片段着色器(Fragment Shader)
  • 测试与混合(Tests and Blending)
image.png
  • 第一阶段,顶点着色器。该阶段的输入是 顶点数据(Vertex Data) 数据,比如以数组的形式传递 3 个 3D 坐标用来表示一个三角形。顶点数据是一系列顶点的集合。顶点着色器主要的目的是把 3D 坐标转为另一种 3D 坐标,同时顶点着色器可以对顶点属性进行一些基本处理。

  • 第二阶段,形状(图元)装配。该阶段将顶点着色器输出的所有顶点作为输入,并将所有的点装配成指定图元的形状。图中则是一个三角形。图元(Primitive) 用于表示如何渲染顶点数据,如:点、线、三角形。

  • 第三阶段,几何着色器。该阶段把图元形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的(或是其它的)图元来生成其他形状。例子中,它生成了另一个三角形。

  • 第四阶段,光栅化。该阶段会把图元映射为最终屏幕上相应的像素,生成片段。片段(Fragment) 是渲染一个像素所需要的所有数据。

  • 第五阶段,片段着色器。该阶段首先会对输入的片段进行 裁切(Clipping)。裁切会丢弃超出视图以外的所有像素,用来提升执行效率。

  • 第六阶段,测试与混合。该阶段会检测片段的对应的深度值(z坐标),判断这个像素位于其它物体的前面还是后面,决定是否应该丢弃。此外,该阶段还会检查 alpha 值( alpha 值定义了一个物体的透明度),从而对物体进行混合。因此,即使在片段着色器中计算出来了一个像素输出的颜色,在渲染多个三角形的时候最后的像素颜色也可能完全不同。

关于混合,GPU 采用如下公式进行计算,并得出最后的颜色

R = S + D * (1 - Sa)

关于公式的含义,假设有两个像素 S(source) 和 D(destination),S 在** z** 轴方向相对靠前(在上面),D 在 z 轴方向相对靠后(在下面),那么最终的颜色值就是 S(上面像素) 的颜色 + D(下面像素) 的颜色 * (1 - S(上面像素) 颜色的透明度)

上述流水线以绘制一个三角形为进行介绍,可以为每个顶点添加颜色来增加图形的细节,从而创建图像。但是,如果让图形看上去更加真实,需要足够多的顶点和颜色,相应也会产生更大的开销。为了提高生产效率和执行效率,开发者经常会使用 纹理(Texture) 来表现细节。纹理是一个 2D 图片(甚至也有 1D 和 3D 的纹理)。纹理 一般可以直接作为图形渲染流水线的第五阶段的输入。

3.屏幕成像与卡顿情况

介绍屏幕图像显示的原理,需要先从 CRT 显示器原理说起,如下图所示。CRT 的电子枪从上到下逐行扫描,扫描完成后显示器就呈现一帧画面。然后电子枪回到初始位置进行下一次扫描。为了同步显示器的显示过程和系统的视频控制器,显示器会用硬件时钟产生一系列的定时信号。当电子枪换行进行扫描时,显示器会发出一个水平同步信号(horizonal synchronization),简称 HSync;而当一帧画面绘制完成后,电子枪回复到原位,准备画下一帧前,显示器会发出一个垂直同步信号(vertical synchronization),简称 VSync。显示器通常以固定频率进行刷新,这个刷新率就是 VSync 信号产生的频率。虽然现在的显示器基本都是液晶显示屏了,但其原理基本一致。

image.png

下图所示为常见的 CPU、GPU、显示器工作方式。CPU 计算好显示内容提交至 GPU,GPU 渲染完成后将渲染结果存入帧缓冲区,视频控制器会按照 VSync 信号逐帧读取帧缓冲区的数据,经过数据转换后最终由显示器进行显示。


image.png

最简单的情况下,帧缓冲区只有一个。此时,帧缓冲区的读取和刷新都都会有比较大的效率问题,并且有可能产生撕裂。

撕裂原因:

  • 在屏幕显示图形图像的过程中,是不断从帧缓存区获取一帧一帧数据进行显示的,
  • 然后在渲染的过程中,帧缓存区中仍是旧的数据,屏幕拿到旧的数据去进行显示,
  • 在旧的数据没有读取完时 ,新的一帧数据处理好了,放入了缓存区,这时就会导致屏幕另一部分的显示是获取的新数据,从而导致屏幕上呈现图片不匹配,人物、景象等错位显示的情况。

撕裂表现为下图:


image.png

为了解决效率和撕裂问题,GPU 通常会引入两个缓冲区,即双缓冲机制。在这种情况下,GPU 会预先渲染一帧放入一个缓冲区中,用于视频控制器的读取。当下一帧渲染完毕后,GPU 会直接把视频控制器的指针指向第二个缓冲器。

image.png

苹果官方针对屏幕撕裂现象,目前一直采用的是垂直同步+双缓存,该方案是强制要求同步,且是以掉帧为代价的。

掉帧图:


image.png

总结:

  • CPU和GPU在渲染的流水线中耗时过长,导致从缓存区获取位图显示时,下一帧的数据还没有准备好,获取的仍是上一帧的数据,产生掉帧现象,掉帧就会导致屏幕卡顿
  • 苹果官方针对屏幕撕裂问题,目前一直使用的方案是垂直同步+双缓存区,可以从根本上防止和解决屏幕撕裂,但是同时也导致了新的问题掉帧。虽然我们采用了双缓存区,但是我们并不能解决CPU和GPU处理图形图像的速度问题,导致屏幕在接收到垂直信号时,数据尚未准备好,缓存区仍是上一帧的数据,因此导致掉帧
  • 在垂直同步+双缓存区的方案上,再次进行优化,将双缓存区,改为三缓存区,这样其实也并不能从根本上解决掉帧的问题,只是比双缓存区掉帧的概率小了很多,仍有掉帧的可能性,对于用户而言,可能是无感知的。

4.iOS渲染框架

图形渲染流程图:

下图所示为 iOS App 的图形渲染流程,App 使用 Core Graphics、Core Animation、Core Image 等框架来绘制可视化内容,这些软件框架相互之间也有着依赖关系。这些框架都需要通过 OpenGL 来调用 GPU 进行绘制,最终将内容显示到屏幕之上。


流程.png
  • App通过调用CoreGraphics、CoreAnimation、CoreImage等框架的接口触发图形渲染操作
  • CoreGraphics、CoreAnimation、CoreImage等框架将渲染交由OpenGL ES或者Metal,由OpenGL ES来驱动GPU做渲染,最后显示到屏幕上
  • 由于OpenGL ES 是跨平台的,所以在他的实现中,是不能有任何窗口相关的代码,而是让各自的平台为OpenGL ES提供载体。在ios中,如果需要使用OpenGL ES,就是通过CoreAnimation提供窗口,让App可以去调用。

iOS渲染框架

框架 说明 介绍
UIKit UIKit 是 iOS 开发者最常用的框架,可以通过设置 UIKit 组件的布局以及相关属性来绘制界面。 UIKit 自身并不具备在屏幕成像的能力,其主要负责对用户操作事件的响应(UIView 继承自 UIResponder),事件响应的传递大体是经过逐层的 视图树 遍历实现的。
Core Animation Core Animation 源自于 Layer Kit,动画只是 Core Animation 特性的冰山一角。 Core Animation 是一个复合引擎,其职责是 尽可能快地组合屏幕上不同的可视内容,这些可视内容可被分解成独立的图层(即 CALayer),这些图层会被存储在一个叫做图层树的体系之中。从本质上而言,CALayer 是用户所能在屏幕上看见的一切的基础。
Core Graphics Core Graphics 基于 Quartz 高级绘图引擎,主要用于运行时绘制图像。开发者可以使用此框架来处理基于路径的绘图,转换,颜色管理,离屏渲染,图案,渐变和阴影,图像数据管理,图像创建和图像遮罩以及 PDF 文档创建,显示和分析 当开发者需要在运行时创建图像 时,可以使用 Core Graphics 去绘制。与之相对的是 运行前创建图像,例如用 Photoshop 提前做好图片素材直接导入应用。相比之下,我们更需要 Core Graphics去在运行时实时计算、绘制一系列图像帧来实现动画。
Core Image Core Image 与 Core Graphics 恰恰相反,Core Graphics 用于在 运行时创建图像,而 Core Image 是用来处理 运行前创建的图像 的。Core Image 框架拥有一系列现成的图像过滤器,能对已存在的图像进行高效的处理。 大部分情况下,Core Image 会在 GPU 中完成工作,但如果 GPU 忙,会使用 CPU 进行处理。
OpenGL ES OpenGL ES(OpenGL for Embedded Systems,简称 GLES),是 OpenGL 的子集。 OpenGL 是一套第三方标准,函数的内部实现由对应的 GPU 厂商开发实现。
Metal Metal 类似于 OpenGL ES,也是一套第三方标准,具体实现由苹果实现。大多数开发者都没有直接使用过 Metal,但其实所有开发者都在间接地使用 Metal。Core Animation、Core Image、SceneKit、SpriteKit 等等渲染框架都是构建于 Metal 之上的。 当在真机上调试 OpenGL 程序时,控制台会打印出启用 Metal 的日志。根据这一点可以猜测,Apple 已经实现了一套机制将 OpenGL 命令无缝桥接到 Metal 上,由 Metal 担任真正于硬件交互的工作

5.CoreAnimation渲染

在前面的Core Animation 简介中提到 CALayer 事实上是用户所能在屏幕上看见的一切的基础。为什么UIKit 中的视图能够呈现可视化内容?就是因为 UIKit中的每一个 UI 视图控件其实内部都有一个关联的 CALayer,即 backing layer

由于这种一一对应的关系,视图层级拥有 视图树 的树形结构,对应 CALayer 层级也拥有 图层树 的树形结构。

image.png

其中,视图的职责是 创建并管理 图层,以确保当子视图在层级关系中 添加或被移除 时,其关联的图层在图层树中也有相同的操作,即保证视图树和图层树在结构上的一致性。

那么为什么 iOS 要基于 UIView 和 CALayer 提供两个平行的层级关系呢?

其原因在于要做职责分离,这样也能避免很多重复代码。在 iOS 和 Mac OS X 两个平台上,事件和用户交互有很多地方的不同,基于多点触控的用户界面和基于鼠标键盘的交互有着本质的区别,这就是为什么 iOS 有 UIKit 和 UIView,对应 Mac OS X 有 AppKit 和 NSView 的原因。它们在功能上很相似,但是在实现上有着显著的区别。

实际上,这里并不是两个层级关系,而是四个。每一个都扮演着不同的角色。除了 视图树 和 图层树,还有 呈现树 和 渲染树。

CALayer

那么为什么 CALayer 可以呈现可视化内容呢?因为 CALayer 基本等同于一个 纹理。纹理是 GPU 进行图像渲染的重要依据。

在 图形渲染原理 中提到纹理本质上就是一张图片,因此 CALayer 也包含一个 contents 属性指向一块缓存区,称为 backing store,可以存放位图(Bitmap)。iOS 中将该缓存区保存的图片称为 寄宿图

image.png

图形渲染流水线支持从顶点开始进行绘制(在流水线中,顶点会被处理生成纹理),也支持直接使用纹理(图片)进行渲染。相应地,在实际开发中,绘制界面也有两种方式:一种是 手动绘制;另一种是 使用图片

对此,iOS 中也有两种相应的实现方式:

  • 使用图片:contents image
  • 手动绘制:custom drawing

Contents Image

Contents Image 是指通过 CALayercontents 属性来配置图片。然而,contents 属性的类型为id。在这种情况下,可以给 contents 属性赋予任何值,app 仍可以编译通过。但是在实践中,如果 content 的值不是 CGImage ,得到的图层将是空白的。

既然如此,为什么要将 contents 的属性类型定义为 id而非 CGImage。这是因为在 Mac OS 系统中,该属性对CGImageNSImage 类型的值都起作用,而在 iOS 系统中,该属性只对CGImage 起作用。

本质上,contents 属性指向的一块缓存区域,称为 backing store,可以存放 bitmap 数据。

Custom Drawing

Custom Drawing 是指使用 Core Graphics 直接绘制寄宿图。实际开发中,一般通过继承 UIView 并实现 -drawRect:方法来自定义绘制。

虽然 -drawRect:是一个 UIView 方法,但事实上都是底层的 CALayer 完成了重绘工作并保存了产生的图片。下图所示为 -drawRect: 绘制定义寄宿图的基本原理。

image.png

  • UIView 有一个关联图层,即 CALayer。
  • CALayer 有一个可选的 delegate 属性,实现了 CALayerDelegate 协议。UIView 作为 CALayer 的代理实现了 CALayerDelegae 协议。
  • 当需要重绘时,即调用 -drawRect:,CALayer 请求其代理给予一个寄宿图来显示。
  • CALayer 首先会尝试调用 -displayLayer: 方法,此时代理可以直接设置 contents 属性。
- (void)displayLayer:(CALayer *)layer;
  • 最后,由 Core Graphics 绘制生成的寄宿图会存入 backing store。

Core Animation 流水线

通过前面的介绍,我们知道了 CALayer 的本质,那么它是如何调用 GPU 并显示可视化内容的呢?下面我们就需要介绍一下 Core Animation 流水线的工作原理。


image.png

事实上,app 本身并不负责渲染,渲染则是由一个独立的进程负责,即 Render Server 进程。

App 通过 IPC 将渲染任务及相关数据提交给Render ServerRender Server 处理完数据后,再传递至 GPU。最后由 GPU 调用 iOS 的图像设备进行显示。

Core Animation 流水线的详细过程如下:

  • 首先,由 app 处理事件(Handle Events),如:用户的点击操作,在此过程中 app 可能需要更新 视图树,相应地,图层树 也会被更新。
  • 其次,app 通过 CPU 完成对显示内容的计算,如:视图的创建、布局计算、图片解码、文本绘制等。在完成对显示内容的计算之后,app 对图层进行打包,并在下一次 RunLoop 时将其发送至 Render Server,即完成了一次Commit Transaction操作。
  • Render Server 主要执行 Open GL、Core Graphics 相关程序,并调用 GPU
  • GPU 则在物理层上完成了对图像的渲染。
  • 最终,GPU 通过 Frame Buffer、视频控制器等相关部件,将图像显示在屏幕上。
    对上述步骤进行串联,它们执行所消耗的时间远远超过 16.67 ms,因此为了满足对屏幕的 60 FPS 刷新率的支持,需要将这些步骤进行分解,通过流水线的方式进行并行执行,如下图所示。
image.png

Commit Transaction

在 Core Animation 流水线中,app 调用 Render Server 前的最后一步 Commit Transaction 其实可以细分为 4 个步骤:

  • Layout
  • Display
  • Prepare
  • Commit

Layout

Layout阶段主要进行视图构建,包括:LayoutSubviews 方法的重载,addSubview: 方法填充子视图等。

Display

Display 阶段主要进行视图绘制,这里仅仅是设置最要成像的图元数据。重载视图的 drawRect: 方法可以自定义 UIView的显示,其原理是在 drawRect: 方法内部绘制寄宿图,该过程使用 CPU 和内存。

Prepare

Prepare阶段属于附加步骤,一般处理图像的解码和转换等操作。

Commit

Commit 阶段主要将图层进行打包,并将它们发送至 Render Server。该过程会递归执行,因为图层和视图都是以树形结构存在

动画渲染原理

iOS 动画的渲染也是基于上述 Core Animation 流水线完成的。这里我们重点关注 app 与Render Server的执行流程。

日常开发中,如果不是特别复杂的动画,一般使用 UIView Animation 实现,iOS 将其处理过程分为如下三部阶段:

  • Step 1:调用 animationWithDuration:animations: 方法
  • Step 2:在 Animation Block 中进行 Layout,Display,Prepare,Commit 等步骤。
  • Step 3:Render Server 根据 Animation 逐帧进行渲染。


    image.png

6.离屏渲染问题

离屏渲染与正常渲染

屏幕上最终显示的数据有两种加载流程

  • 正常渲染加载流程
  • 离屏渲染加载流程
image.png

从图上看,他们之间的区别就是离屏渲染比正常渲染多了一个离屏缓冲区,这个缓冲区的作用是什么呢?

正常渲染流程

APP中的数据经过CPU计算和GPU渲染后,将结果存放在帧缓冲区,利用视频控制器从帧缓冲区中取出,并显示到屏幕上。

  • 在GPU的渲染流程中,显示到屏幕上的图像是遵循大画家算法按照由远及近的顺序,依次将结果存储到帧缓冲区
  • 视屏控制器从帧缓冲区中读取一帧数据,将其显示到屏幕上后,会立即丢弃这帧数据,不会做任何保留,这样做的目的是可以节省空间,且在屏幕上是各自显示各自的,互相不影响。
image.png

离屏渲染流程

当App需要进行额外的渲染和合并时,例如按钮设置圆角,我们是需要对UIButton这个控件中的所有图层都进行圆角+裁剪,然后再将合并后的结果存入帧缓存区,再从帧缓存中取出交由屏幕显示,这时,在正常的渲染流程中,我们是无法做到对所有图层进行圆角裁剪的,因为它是用一个丢一个。所以我们需要提前将处理好的结果放入离屏缓冲区,最后将几个图层进行叠加合并,存放到站缓冲区,最后屏幕上就是我们想实现的效果。

image.png

所以离屏缓存区就是一个临时的缓冲区,用来存放在后续操作使用,但目前并不使用的数据。

  • 离屏渲染再给我们带来方便的同时,也带来了严重的性能问题。由于离屏渲染中的离屏缓冲区,是额外开辟的一个存储空间,当它将数据转存到Frame Buffer时,也是需要耗费时间的,所以在转存的过程中,仍有掉帧的可能。
  • 离屏缓冲区的空间并不是无限大的, 它是又上限的,最大只能是屏幕的2.5倍

那为什么我们明知有性能问题时,还是要使用离屏渲染呢?

  • 可以处理一些特殊的效果,这种效果并不能一次就完成,需要使用离屏缓冲区来保存中间状态,不得不使用离屏渲染,这种情况下的离屏渲染是系统自动触发的,例如经常使用的圆角、阴影、高斯模糊、光栅化等
  • 可以提升渲染的效率,如果一个效果是多次实现的,可以提前渲染,保存到离屏缓冲区,以达到复用的目的。这种情况是需要开发者手动触发的。

离屏渲染的另一个原因:光栅化

When the value of this property is YES, the layer is rendered as a bitmap in its local coordinate space and then composited to the destination with any other content.

当我们开启光栅化时,会将layer渲染成位图保存在缓存中,这样在下次使用时,就可以直接复用,提高效率。
针对光栅化的使用,有以下几个建议:

  • 如果layer不能被复用,则没有必要开启光栅化
  • 如果layer不是静态,需要被频繁修改(例如动画过程中),此时开启光栅化反而影响效率
  • 离屏渲染缓存内容有时间限制,如果100ms内没有被使用,那么就会丢弃,无法进行复用
  • 离屏渲染的缓存空间有限,是屏幕的2.5倍,超过2.5倍屏幕像素大小的话也会失效,无法实现复用

圆角中离屏渲染的触发时机

在讲圆角之前,首先说明下CALayer的构成,如图所示,它是由backgroundColorcontentsborderWidth&borderColor构成的。跟我们即将解释的圆角触发离屏渲染息息相关。

image.png

圆角设置不生效问题!

在平常写代码时,比如UIButton设置圆角,当设置好按钮的image、cornerRadius、borderWidth、borderColor等属性后,运行发现并没有实现我们想要的效果

        let btn0 = UIButton(type: .custom)
        btn0.frame = CGRect(x: 100, y: 60, width: 100, height: 100)
        //设置圆角
        btn0.layer.cornerRadius = 50
        //设置border宽度和颜色
        btn0.layer.borderWidth = 2
        btn0.layer.borderColor = UIColor.red.cgColor
        self.view.addSubview(btn0)
        //设置背景图片
        btn0.setImage(UIImage(named: "mouse"), for: .normal)
image.png

可以发现,我们设置的按钮图片还是方方正正的

下面是苹果官方文档针对圆角设置的一些说明:


image.png

官方文档告诉我们,设置cornerRadius只会对CALayer中的backgroundColorboder设置圆角,不会设置contents的圆角,如果contents需要设置圆角,需要同时将maskToBounds / clipsToBounds设置为true。

所以我们可以理解为圆角不生效的根本原因是没有对contents设置圆角,而按钮设置的image是放在contents里面的,所以看到的界面上的就是image没有进行圆角裁剪。

下面我们通过几段代码来说明 圆角设置中什么时候会离屏渲染触发
首先,需要打开模拟器的离屏渲染颜色标记

image.png

1、按钮 仅设置背景颜色+border

        let btn01 = UIButton(type: .custom)
        btn01.frame = CGRect(x: 100, y: 200, width: 100, height: 100)
        //设置圆角
        btn01.layer.cornerRadius = 50
        //设置border宽度和颜色
        btn01.layer.borderWidth = 4
        btn01.layer.borderColor = UIColor.red.cgColor
        self.view.addSubview(btn01)
        //设置背景颜色
        btn01.backgroundColor = UIColor.green

在这种情况下,无论是使用默认的maskToBounds / clipsToBounds(false),还是将其修改为true,都不会触发离屏渲染,究其根本原因是contents中没有需要圆角处理的layer

2:按钮设置背景图片+boder

        let btn0 = UIButton(type: .custom)
        btn0.frame = CGRect(x: 100, y: 60, width: 100, height: 100)
        //设置圆角
        btn0.layer.cornerRadius = 50
        //设置border宽度和颜色
        btn0.layer.borderWidth = 2
        btn0.layer.borderColor = UIColor.red.cgColor
        self.view.addSubview(btn0)
        //设置背景图片
        btn0.setImage(UIImage(named: "mouse"), for: .normal)
  • 使用默认的maskToBounds / clipsToBounds(false)
    这种情况就是最开始我们讲到的圆角设置不生效的情况,就不再多做说明了

  • maskToBounds / clipsToBounds 修改为true

    image.png

从屏幕的显示上可以看出,此时触发了离屏渲染,是因为圆角的设置是需要对所有layer都进行裁剪的,而maskToBounds裁剪是应用到所有layer上的。如果从正常渲染的角度来说,一个个layer是用完即扔的。而现在我们的圆角设置需要3个layer叠加合并的,所以将先处理好的layer保存在离屏缓冲区,等到最后一个layer处理完,合并进行圆角+裁剪,所以才会触发离屏渲染

总结

  • 当只设置backgroundColor、border,而contents中没有子视图时,无论maskToBounds / clipsToBounds是true还是false,都不会触发离屏渲染
  • 当contents中有子视图时,此时设置 cornerRadius+maskToBounds / clipsToBounds,就会触发离屏渲染
  • UIImageView中只设置图片+maskToBounds / clipsToBounds是不会触发离屏渲染,苹果对UIImageView优化只是将image直接画在了contents上面这样不设置背景色其实只需要渲染一个layer,所以不需要用到离屏缓冲区

常见触发离屏渲染的几种情况:

    1. 使⽤了 mask 的 layer (layer.mask)
    1. 需要进⾏裁剪的 layer (layer.masksToBounds / view.clipsToBounds)
    1. 设置了组透明度为 YES,并且透明度不为 1 的 layer (layer.allowsGroupOpacity/
      layer.opacity)
    1. 添加了投影的 layer (layer.shadow)
    1. 采⽤了光栅化的 layer (layer.shouldRasterize)
    1. 绘制了⽂字的 layer (UILabel, CATextLayer, Core Text 等)

参考

  1. 离屏渲染优化详解:实例示范+性能测试
  2. Mastering Offscreen Render
  3. Optimizing 2D Graphics and Animation Performance
  4. Polishing Your Interface Rotation Animations
  5. Core Animation Essentials
  6. Understanding UIKit Rendering
  7. iOS: Rendering the UI
  8. iOS 事件处理机制与图像渲染过程
  9. iOS 动画篇:核心动画
  10. GPU Framebuffer Memory: Understanding Tiling
  11. iOS 保持界面流畅的技巧
  12. OpenGL ES 框架详细解析(八) —— OpenGL ES 设计指南
  13. iOS 开发-视图渲染与性能优化
  14. iOS 视图、动画渲染机制探究
  15. iOS 事件处理机制与图像渲染过程
  16. iOS界面渲染流程
  17. 界面渲染的整体流程
  18. iOS图像处理之Core Graphics和OpenGL ES初见

你可能感兴趣的:(关于图形渲染以及离屏渲染)