【OpenGL入门】iOS下的图像渲染原理

ios下的图像渲染原理

CPU与GPU

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

使用 GPU 渲染图形的根本缘由就是速度问题。GPU 优秀的并行计算能力使其可以快速将图形结果计算出来并在屏幕的全部像素中进行显示。ios

屏幕图像的显示原理

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

屏幕图像的显示原理

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

CPU、GPU、显示器工做方式

双缓冲机制

因此,显示一个画面须要两步完成:框架

  • CPU把须要显示的画面数据计算出来
  • 显示器把这些数据显示出来

这两步工做都须要时间,而且能够并行执行,由于具体执行这两个过程的硬件是相互独立的(CPU/显卡显示控制器)。可是这两个工做的耗时是不一样的。 CPU 以及显卡每秒能计算出的画面数量是根据硬件性能决定的。 可是显示器每秒刷新频率是固定的(通常是60hz,因此每隔16.667ms就会刷新一次)。函数

因为存在两边速率不统一的问题,因此引入了帧缓冲区(FrameBuffer)的概念。oop

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

根据苹果的官方文档描述,iOS 设备会始终使用 Vsync + Double Buffering(垂直同步+双缓冲) 的策略。性能

双缓冲机制

屏幕撕裂

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

屏幕撕裂

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

drawing-with-vsync

drawing-without-vsync

掉帧

开启了垂直同步后,理想情况下 CPU 和 GPU 能够在16ms内处理完每一帧的渲染。可是若是显卡的帧率小于屏幕的刷新率,CPU 和 GPU 处理完一帧的渲染的时间超过了16ms,就会发生掉帧的状况。那一帧会被丢弃,等待下一次机会再显示,而这时显示屏会保留以前的内容不变。这就是界面卡顿的缘由。

此时显示控制器占用一个 Buffer ,GPU 占用一个 Buffer 。两个Buffer都被占用,致使 CPU 空闲下来浪费了资源,由于垂直同步的缘由,只有到了 VSync 信号的时间点, CPU 才能触发绘制工做。

double-buffering

三缓冲机制

在Android4.1系统开始,引入了三缓冲+垂直同步的机制。因为多加了一个 Buffer,实现了 CPU 跟 GPU 并行,即可以作到了只在开始掉一帧,后续却不掉帧,双缓冲充分利用16ms作到低延时,三缓冲保障了其稳定性。

triple-buffering

iOS的渲染框架

iOS App 的图形渲染使用了 Core GraphicsCore AnimationCore Image 等框架来绘制可视化内容,这些软件框架相互之间也有着依赖关系。这些框架都须要经过 OpenGL 来调用 GPU 进行绘制,最终将内容显示到屏幕之上。

ios-rendering-framework-relationship

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 ImageCore 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,但其实全部开发者都在间接地使用 MetalCore AnimationCore ImageSceneKitSpriteKit 等等渲染框架都是构建于 Metal 之上的。

当在真机上调试 OpenGL 程序时,控制台会打印出启用 Metal 的日志。根据这一点能够猜想,Apple 已经实现了一套机制将 OpenGL 命令无缝桥接到 Metal 上,由 Metal 担任真正于硬件交互的工做。

UIView 与 CALayer

CALayer 是用户所能在屏幕上看见的一切的基础,用来存放位图(Bitmap)UIKit 中的每个 UI 视图控件(UIView)其实内部都有一个关联的 CALayer,即 backing layer

因为这种一一对应的关系,视图(UIView)层级拥有 视图树 的树形结构,对应 CALayer 层级也拥有 图层树 的树形结构。

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

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

其缘由在于要作 职责分离,这样也能避免不少重复代码。在 iOSMac OS X 两个平台上,事件和用户交互有不少地方的不一样,基于多点触控的用户界面和基于鼠标键盘的交互有着本质的区别,这就是为何 iOSUIKitUIView,对应 Mac OS XAppKitNSView 的缘由。它们在功能上很类似,可是在实现上有着显著的区别。

CALayer

CALayer.h 中,CALayer 有这样一个属性 contents

/** Layer content properties and methods. **/

/* An object providing the contents of the layer, typically a CGImageRef, * but may be something else. (For example, NSImage objects are * supported on Mac OS X 10.6 and later.) Default value is nil. * Animatable. */

@property(nullable, strong) id contents;
复制代码

contents 提供了 layer 的内容,是一个指针类型,在 iOS 中的类型就是 CGImageRef(在 OS X 中还能够是 NSImage)。CALayer 中的 contents 属性保存了由设备渲染流水线渲染好的位图 bitmap(一般也被称为 backing store),而当设备屏幕进行刷新时,会从 CALayer 中读取生成好的 bitmap,进而呈现到屏幕上。

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

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

Contents Image Contents Image 是指经过 CALayercontents 属性来配置图片。然而,contents属性的类型为 id。在这种状况下,能够给 contents 属性赋予任何值,app 仍能够编译经过。可是在实践中,若是 content 的值不是 CGImage ,获得的图层将是空白的。

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

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

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

ios-layer-bitmap-custom-drawing

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

UIView 是 app 中的基本组成结构,定义了一些统一的规范。它会负责内容的渲染以及,处理交互事件。

  • Drawing and animation:绘制与动画
  • Layout and subview management:布局与子 view 的管理
  • Event handling:点击事件处理

CALayerUIView 的属性之一,负责渲染和动画,提供可视内容的呈现。UIView 提供了对 CALayer 部分功能的封装,同时也另外负责了交互事件的处理。

  • 相同的层级结构:咱们对 UIView 的层级结构很是熟悉,因为每一个 UIView 都对应 CALayer负责页面的绘制,因此 CALayer 也具备相应的层级结构。

  • 部分效果的设置:由于 UIView 只对 CALayer 的部分功能进行了封装,而另外一部分如圆角、阴影、边框等特效都须要经过调用 layer 属性来设置。

  • 是否响应点击事件:CALayer 不负责点击事件,因此不响应点击事件,而 UIView 会响应。

  • 不一样继承关系:CALayer 继承自 NSObjectUIView 因为要负责交互事件,因此继承自 UIResponder

Core Animation

Core Animation 流水线

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

ios-core-animation-pipeline-steps

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

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

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

ios-core-animation-pipeline-workflow

图层树

CoreAnimation做为一个复合引擎,将不一样的视图层组合在屏幕中,而且存储在图层树中,向咱们展现了全部屏幕上的一切。

整个过程其实经历了三个树状结构,才显示到了屏幕上:模型树-->呈现树-->渲染树

层级关系树中除了 视图树图层树,还有 呈现树渲染树。他们各自都有各自的职责。

  • 呈现树:咱们能够经过CALayer-presentationLayer方法来访问对应的呈现树图层。注意呈现图层仅仅当图层首次被提交(就是首次第一次在屏幕上显示)的时候建立,因此在那以前调用-presentationLayer将会返回nil。
- (nullable instancetype)presentationLayer;
复制代码
  • 模型树:在呈现图层上调用–modelLayer将会返回它正在呈现所依赖的CALayer。一般在一个图层上调用-modelLayer会返回self(实际上咱们已经建立的原始图层就是一种数据模型)。
- (instancetype)modelLayer;
复制代码

一般,咱们操做的是模型树modelLayer,在重绘周期最后,咱们会将模型树相关内容(层次结构、图层属性和动画)序列化,经过IPC传递给专门负责屏幕渲染的渲染进程。渲染进程拿到数据并反序列化出树状结构--呈现树。这个呈现图层其实是模型图层的复制,可是它的属性值表明了在任何指定时刻当前外观效果。换句话说,能够经过呈现图层的值来获取当前屏幕上真正显示出来的值。

当模型树modelLayer上带有动画特征时,提交到渲染进程后,渲染进程会根据动画特征,不断修改呈现树presentationLayer上的图层属性,并同时不断的在屏幕上渲染出来,这样咱们就看到了动画。

若是想让动画的图层响应用户输入,可使用-hitTest:方法来判断指定图层是否被触摸,这时候对呈现图层而不是模型图层调用-hitTest:会显得更有意义,由于呈现图层表明了用户当前看到的图层位置,而不是当前动画结束以后的位置。

能够理解为modelLayer负责数据的存储和获取,presentationLayer负责显示。每次屏幕刷新的时候,presentationLayer会与modelLayer状态同步。

CAAnimation加到layer上以后,presentationLayer每次刷新的时候会去CAAnimation询问并同步状态,CAAnimation控制presentationLayerfromValuetoValue来改变值,而动画结束以后,CAAnimation会从layer上被移除,此时屏幕刷新的时候presentationLayer又会同步modelLayer的状态,modelLayer没有改变,因此又回到了起点。固然咱们能够经过设置,继续影响presentationLayer的状态。

Core Animation 动画

Core Animation动画,即基于事务的动画,是最多见的动画实现方式。动画执行者是专门负责渲染的渲染进程,操做的是呈现树。咱们应该尽可能使用Core Animation来控制动画,由于Core Animation是充分优化过的:

基于Layer的绘图过程当中,Core Animation经过硬件操做位图(变换、组合等),产生动画的速度比软件操做的方式快不少。

基于View的绘图过程当中,view被改动时会触发的drawRect:方法来从新绘制位图,可是这种方式须要CPU在主线程执行,比较耗时。而Core Animation则尽量的操做硬件中已缓存的位图,来实现相同的效果,从而减小了资源损耗。

非 Core Animation 动画

CoreA nimation动画执行者是当前进程,操做的是模型树。常见的有定时器动画和手势动画。定时器动画是在定时周期触发时修改模型树的图层属性;手势动画是手势事件触发时修改模型树的图层属性。二者都能达到视图随着时间不断变化的效果,即实现了动画。

Core Animation动画动画过程当中实际上不断改动的是模型树,而呈现树仅仅成了模型树的复制品,状态与模型树保持一致。整个过程当中,主要是CPU在主线程不断调整图层属性、布局计算、提交数据,没有充分利用到Core Animation强大的动画控制功能。

相关文章
相关标签/搜索