学计算机的那个

不是我觉到、悟到,你给不了我,给了也拿不住;只有我觉到、悟到,才有可能做到,能做到的才是我的.

0%

iOS渲染流程

前言

  1. app中一个视图从点击屏幕(硬件)到完全渲染,中间发生了什么?
  2. 在一个表内有很多cell,每个cell上有很多个视图,如何解决卡顿问题?
  3. UIView与CALayer的区别?

CPU GPU

CPU(Central Processing Unit):作为整个系统的运算和控制单元。CPU 内部的流水线结构使其拥有一定程度的并行计算能力。

GPU(Graphics Processing Unit):一种可进行绘图运算工作的专用微处理器。GPU 能够生成 2D/3D 的图形图像和视频,从而能够支持基于窗口的操作系统、图形用户界面、视频游戏、可视化图像应用和视频播放。GPU 具有非常强的并行计算能力。

使用 GPU 渲染图形的根本原因就是:速度。GPU 的并行计算能力使其能够快速将图形结果计算出来并在屏幕的所有像素中进行显示

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

计算机将存储在内存中的形状转换成实际绘制在屏幕上的对应的过程称为渲染

渲染过程中最常用的技术就是 光栅化

光栅化就是将数据转化成可见像素的过程

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

GPU历史

GPU 还未出现前,PC 上的图形操作是由视频图形阵列(VGA,Video Graphics Array) 控制器完成。VGA 控制器由连接到一定容量的DRAM上的存储控制器和显示产生器构成。

1997 年,VGA 控制器开始具备一些 3D 加速功能,包括用于 三角形生成光栅化纹理贴图 和 阴影

如今,GPU 及其相关驱动实现了图形处理中的 OpenGL 和 DirectX 模型,从而允许开发者能够轻易地操作硬件。

  • GPU 图形渲染流水线的主要工作可以被划分为两个部分:
  1. 把 3D 坐标转换为 2D 坐标
  2. 把 2D 坐标转变为实际的有颜色的像素
  • GPU 图形渲染流水线的具体实现可分为六个阶段,如下图所示。

  • 顶点着色器(Vertex Shader)

  • 形状装配(Shape Assembly),又称 图元装配

  • 几何着色器(Geometry Shader)

  • 光栅化(Rasterization)

  • 片段着色器(Fragment Shader)

  • 测试与混合(Tests and Blending)

CPU-GPU 异构系统

两种常见的 CPU-GPU 异构架构

左图是分离式的结构,CPU 和 GPU 拥有各自的存储系统,两者通过 PCI-e 总线进行连接。这种结构的缺点在于 PCI-e 相对于两者具有低带宽和高延迟,数据的传输成了其中的性能瓶颈。目前使用非常广泛,如PC、智能手机等。

右图是耦合式的结构,CPU 和 GPU 共享内存和缓存。AMD 的 APU 采用的就是这种结构,目前主要使用在游戏主机中,如 PS4

CPU-GPU 工作流

下图所示为 CPU-GPU 异构系统的工作流,当 CPU 遇到图像处理的需求时,会调用 GPU 进行处理,主要流程可以分为以下四步:

  1. 将主存的处理数据复制到显存中
  2. CPU 指令驱动 GPU
  3. GPU 中的每个运算单元并行处理
  4. GPU 将显存结果传回主存

屏幕图像显示原理

CRT 显示器原理

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

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

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

双缓冲虽然能解决效率问题,但会引入一个新的问题。当视频控制器还未读取完成时,即屏幕内容刚显示一半时,GPU 将新的一帧内容提交到帧缓冲区并把两个缓冲区进行交换后,视频控制器就会把新的一帧数据的下半段显示到屏幕上,造成画面撕裂现象.

为了解决这个问题,GPU 通常有一个机制叫做垂直同步(简写也是 V-Sync),当开启垂直同步后,GPU 会等待显示器的 VSync 信号发出后,才进行新的一帧渲染和缓冲区更新。这样能解决画面撕裂现象,也增加了画面流畅度,但需要消费更多的计算资源,也会带来部分延迟。

iOS图形渲染技术栈

简单来说,OpenGL ES是对图层进行取色,采样,生成纹理,绑定数据,生成前后帧缓存。

iOS 渲染框架

UIKit

UIkit 自身不具备在屏幕成像的能力,其主要负责对用户操作事件的响应(UIView 继承自 UIResponder),事件响应的传递大体是经过逐层的视图树遍历实现。

Core Animation

Core Animation源自于 Layer Kit, Core Animation 是一个复合引擎,其职责是 尽可能快地组合屏幕上不同的可视内容,这些可视内容可被分解成独立的图层(即 CALayer),这些图层会被存储在一个叫做图层树的体系之中。从本质上而言,CALayer 是用户所能在屏幕上看见的一切的基础。

Core Graphics

Core Graphics 基于 Quartz 高级绘图引擎,主要用于运行时绘制图像。开发者可以使用此框架来处理基于路径的绘图,转换,颜色管理,离屏渲染,图案,渐变和阴影,图像数据管理,图像创建和图像遮罩以及 PDF 文档创建,显示和分析。

Core Image

Core Image 是用来处理 运行前创建的图像 ,Core Image 框架拥有一系列现成的图像过滤器,能对已存在的图像进行高效的处理。

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 担任真正于硬件交互的工作。

UIView 与 CALayer 的关系

为什么 UIKit 中的视图能够呈现可视化内容?

因为 UIKit 中的每一个 UI 视图控件其实内部都有一个关联的 CALayer,即 backing layer

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

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

为什么 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 中将该缓存区保存的图片称为 寄宿图

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

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

  1. 使用图片:contents image
  2. 手动绘制:custom drawing

Contents Image

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

Custom Drawing

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

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

  1. UIView 有一个关联图层,即 CALayer。
  2. CALayer 有一个可选的 delegate 属性,实现了 CALayerDelegate 协议。UIView 作为 CALayer 的代理实现了 CALayerDelegae 协议。
  3. 当需要重绘时,即调用 -drawRect:,CALayer 请求其代理给予一个寄宿图来显示。
  4. CALayer 首先会尝试调用 -displayLayer: 方法,此时代理可以直接设置 contents 属性。
1
- (void)displayLayer:(CALayer *)layer;
  1. 如果代理没有实现 -displayLayer: 方法,CALayer 则会尝试调用 -drawLayer:inContext: 方法。在调用该方法前,CALayer 会创建一个空的寄宿图(尺寸由 bounds 和 contentScale 决定)和一个 Core Graphics 的绘制上下文,为绘制寄宿图做准备,作为 ctx 参数传入。
1
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;
  1. 最后,由 Core Graphics 绘制生成的寄宿图会存入 backing store

Core Animation 流水线

CALayer是如何调用 GPU 并显示可视化内容的呢?

Core Animation 流水线的工作原理

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

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

  • Core Animation 流水线的详细过程如下:
  1. 首先,由 app 处理事件(Handle Events),如:用户的点击操作,在此过程中 app 可能需要更新 视图树,相应地,图层树 也会被更新。
  2. 其次,app 通过 CPU 完成对显示内容的计算,如:视图的创建、布局计算、图片解码、文本绘制等。在完成对显示内容的计算之后,app 对图层进行打包,并在下一次 RunLoop 时将其发送至 Render Server,即完成了一次 Commit Transaction 操作。
  3. Render Server 主要执行 Open GL、Core Graphics 相关程序,并调用 GPU
  4. GPU 则在物理层上完成了对图像的渲染。
  5. 最终,GPU 通过 Frame Buffer、视频控制器等相关部件,将图像显示在屏幕上。

对上述步骤进行串联,它们执行所消耗的时间远远超过 16.67 ms,因此为了满足对屏幕的 60 FPS 刷新率的支持,需要将这些步骤进行分解,通过流水线的方式进行并行执行,如下图所示。

Commit Transaction

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

  1. Layout
  2. Display
  3. Prepare
  4. Commit
  • Layout

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

  • Display

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

  • Prepare

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

  • Commit

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

动画渲染原理

iOS 动画的渲染也是基于上述 Core Animation 流水线完成的。

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

Step 1:调用 animationWithDuration:animations: 方法

Step 2:在 Animation Block 中进行 Layout,Display,Prepare,Commit 等步骤。

Step 3:Render Server 根据 Animation 逐帧进行渲染。

UIView渲染

iOS渲染视图的核心是Core Animation
其渲染层次依次为:图层树->呈现树->渲染树

CPU阶段

  1. 布局(Frame)
  2. 显示(Core Graphics)
  3. 准备(QuartzCore/Core Animation)
  4. 通过IPC提交(打包好的图层树以及动画属性)

OpenGL ES阶段

  1. 生成(Generate)
  2. 绑定(Bind)
  3. 缓存数据(Buffer Data)
  4. 启用(Enable)
  5. 设置指针(Set Pointers)
  6. 绘图(Draw)
  7. 清除(Delete)

GPU阶段

  1. 接收提交的纹理(Texture)和顶点描述(三角形)
  2. 应用变换(transform)
  3. 合并渲染(离屏渲染等)

总结

  1. 首先一个视图由CPU进行Frame布局,准备视图和图层的层级关系,查询是否有重写drawRect:或drawLayer:inContext:方法,注意:如果有重写的话,这里的渲染是会占用CPU进行处理的
  2. CPU会将处理视图和图层的层级关系打包,通过IPC(内部处理通信)通道提交给渲染服务,渲染服务由OpenGL ES和GPU组成。
  3. 渲染服务首先将图层数据交给OpenGL ES进行纹理生成和着色。生成前后帧缓存,再根据显示硬件的刷新频率,一般以设备的VSync信号和CADisplayLink为标准,进行前后帧缓存的切换。
  4. 最后,将最终要显示在画面上的后帧缓存交给GPU,进行采集图片和形状,运行变换,应用纹理和混合。最终显示在屏幕上。

iOS平台渲染核心原理的重点主要围绕前后帧缓存、Vsync信号、CADisplayLink

Core Animation

core Animation不仅仅是字面意思的核心动画,而是整个显示核心QuartzCore框架中的Core Animation

Core Animation是依赖于OpenGL ES做GPU渲染,CoreGraphics做CPU渲染

1
2
3
4
5
6
7
Core Animation 在 RunLoop 中注册了一个 Observer,监听了 BeforeWaiting 和 Exit 事件。这个Observer的
优先级是2000000,低于常见的其他 Observer。当一个触摸事件到来时,RunLoop 被唤醒,App 中的代码会执行一些操
作,比如创建和调整视图层级、设置UIView的frame、修改CALayer的透明度、为视图添加一个动画;这些操作最终都会被
CALayer 捕获,并通过CATransaction提交到一个中间状态去(CATransaction 的文档略有提到这些内容,但并不完
整)。当上面所有操作结束后,RunLoop 即将进入休眠(或者退出)时,关注该事件的 Observer 都会得到通知。这时 CA
注册的那个 Observer 就会在回调中,把所有的中间状态合并提交到GPU去显示;如果此处有动画,CA会通过DisplayLink
等机制多次触发相关流程。

CPU渲染职能

CPU渲染职能主要体现在以下5个方面:

  1. 布局计算

  2. 视图懒加载

  3. Core Graphics绘制
    如果对视图实现了drawRect:或drawLayer:inContext:方法,或者 CALayerDelegate 的 方法,那么在绘制任何东 西之前都会产生一个巨大的性能开销。为了支持对图层内容的任意绘制,Core Animation必须创建一个内存中等大小的寄宿图片。然后一旦绘制结束之后, 必须把图片数据通过IPC传到渲染服务器。在此基础上,Core Graphics绘制就会变得十分缓慢,所以在一个对性能十分挑剔的场景下这样做十分不好。

  4. 解压图片

  5. 图层打包

GPU渲染职能

GPU会根据生成的前后帧缓存数据,根据实际情况进行合成,其中造成GPU渲染负担的一般是:离屏渲染,图层混合,延迟加载。

前后帧缓存 & Vsync信号

iOS 的显示系统是由 VSync 信号驱动的,VSync 信号由硬件时钟生成,每秒钟发出 60 次(这个值取决设备硬件,比如 iPhone 真机上通常是 59.97)。iOS 图形服务接收到 VSync 信号后,会通过 IPC 通知到 App 内。App 的 Runloop 在启动后会注册对应的 CFRunLoopSource 通过 mach_port 接收传过来的时钟信号通知,随后 Source 的回调会驱动整个 App 的动画与显示。

帧缓存:接收渲染结果的缓冲区,为GPU指定存储渲染结果的区域

帧缓存可以同时存在多个,但是屏幕显示像素受到保存在前帧缓存(front frame buffer)的特定帧缓存中的像素颜色元素的控制。程序的渲染结果通常保存在后帧缓存(back frame buffer)在内的其他帧缓存,当渲染后的后帧缓存完成后,前后帧缓存会互换。(OS完成)

前帧缓存决定了屏幕上显示的像素颜色,会在适当的时候与后帧缓存切换。

Core Animation的合成器会联合OpenGL ES层和UIView层、StatusBar层等,在后帧缓存混合产生最终的颜色,并切换前后帧缓存;
OpenGL ES坐标是以浮点数来存储,即使是其他数据类型的顶点数据也会被转化成浮点型。

参考

  1. 图形图像渲染原理
  2. iOS 图像渲染原理