一、iOS渲染架构
下图分别是iOS渲染早期架构和最新架构,可以看到在最新的架构中使用了Metal代替OpenGL ES。
Graphics Hardware图形处理硬件一般指GPU,在iOS中Core Graphics会使用CPU进行渲染处理(后文会详细说明)。
UIKit
UIKit为iOS应用程序构建和管理图形化、事件驱动的用户界面提供所需的基础结构。
UIKit中视图的显示、动画都通过CoreAnimation来实现。
在UIKit中,UIView类本身在绘制时自动创建一个图形环境,即Core Graphics层的CGContext类型,作为当前的图形绘制环境。可以在drawRect:方法中调用 UIGraphicsGetCurrentContext 函数获得当前的图形环境(CGContextRef)进行绘制。
每一个UIView都有一个CALayer实例的图层属性,也就是所谓的backing layer,视图的职责就是创建并管理这个图层,以确保当子视图在层级关系中添加或者被移除的时候,他们关联的图层也同样对应在层级关系树当中有相同的操作。
下图是iOS渲染更详细的架构图。
Core Animation
Core Animation,它本质上可以理解为一个复合引擎,主要职责包含:渲染、构建和实现动画。
Core Animation的核心是CALayer对象,用来管理和操作显示内容。通常我们会使用 Core Animation 来高效、方便地实现动画,但是实际上它的前身叫做 Layer Kit,关于动画实现只是它功能中的一部分。对于 iOS app,不论是否直接使用了 Core Animation,它都在底层深度参与了 app 的构建。
Core Animation依赖于OpenGL ES做GPU渲染,CoreGraphics做CPU渲染,但CoreGraphics最终也会将CPU渲染的结果通过OpenGL ES交给GPU。
事务CATransaction
事务实际上是Core Animation用来包含一系列属性动画集合的机制,任何用指定事务去改变可以做动画的图层属性都不会立刻发生变化,而是当事务一旦提交的时候开始用一个动画过渡到新值。
事务是通过CATransaction类来做管理,这个类的设计有些奇怪,不像你从它的命名预期的那样去管理一个简单的事务,而是管理了一叠你不能访问的事务。CATransaction没有属性或者实例方法,并且也不能用+alloc和-init方法创建它。但是可以用+begin和+commit分别来入栈或者出栈。
任何可以做动画的图层属性都会被添加到栈顶的事务,你可以通过+setAnimationDuration:方法设置当前事务的动画时间,或者通过+animationDuration方法来获取值(默认0.25秒)。
Core Animation在每个run loop周期中自动开始一次新的事务,即使你不显式的用[CATransaction begin]开始一次事务,任何在一次run loop循环中属性的改变都会被集中起来,然后做一次0.25秒的动画。
隐式动画
隐式动画是系统框架自动完成的。Core Animation在每个runloop周期中自动开始一次新的事务,即使你不显式的用[CATransaction begin]开始一次事务,任何在一次runloop循环中属性的改变都会被集中起来,然后做一次0.25秒的动画。
Core Animation通常对CALayer的所有属性(可动画的属性)做动画,但是UIView是怎么把它关联的图层的这个特性关闭了呢?
每个UIView对它关联的图层都扮演了一个委托,并且提供了-actionForLayer:forKey的实现方法。当不在一个动画块的实现中,UIView对所有图层行为返回nil,但是在动画block范围之内,它就返回了一个非空值。
Core Graphics
Core Graphics是一套C-based API, 支持向量图形,线、形状、图案、路径、剃度、位图图像和pdf 内容的绘制。
Core Graphics中使用的图形环境由一个类CGContext表示。
对于移动平台,有三种常见的图形环境Context:
位图上下文(A bitmap graphics context):一般用于绘制图片或者自定义控件。
View Graphics Context: 由UIView自动创建,你重写UIView drawRect:方法时,你的内容会画在这个上下文上。
Bitmap Graphics Context: 绘制在该上下文的内容会以点阵形式存储在一块内存中。简单说,就是为图片开辟一块内存,然后在里面画东西,上下文帮你把图片内存抽象成一个Context(图层)了。(相关函数:CGBitmapContextCreate)
PDF上下文(A PDF graphics context):用于生成pdf文件。(相关函数:UIGraphicsBeginPDFContextToFile)
图层上下文(A layer context):用于离屏绘制( offscreen drawing)。(相关函数:UIGraphicsBeginImageContext)
当Main Runloop进入beforeWaiting/Exit状态时,会检测所有开启了setNeedsDisplay设置的UIView/CALayer,并触发其drawRect:方法,更新界面。
OpenGL
OpenGL是用于渲染2D、3D矢量图形的跨语言、跨平台的应用程序编程接口(API),各个厂商GPU都支持OpenGL接口标准。
OpenGL工作于硬件抽象层,抽象出硬件加速能力(GPU),应用层开发者不需要考虑不同GPU硬件移植性的问题。
OpenGL ES
ES代表Embedded Systems嵌入式系统,是OpenGL的一套适用于嵌入式设备高性能API子集。OpenGL ES是一套多功能开放标准的用于嵌入系统的C-based的图形库,用于2D和3D数据的可视化。OpenGL被设计用来转换一组图形调用功能到底层图形硬件(GPU),由GPU执行图形命令,用来实现复杂的图形操作和运算,从而能够高性能、高帧率利用GPU提供的2D和3D绘制能力。
OpenGL ES规范本身不定义绘制表面和绘制窗口,因此ios为了使用它必须提供和创建一个OpenGL ES 的呈现环境,创建和配置存储绘制命令结果的framebuffer 及创建和配置一个或多个呈现目标。
渲染管线
渲染管线又称渲染流水线,它是图形图像从数据一步一步形成最终输出的画面所要经历的各种操作过程。数据经过一个操作后,被处理成下一个步骤需要的数据,最终一步一步整合成拼凑最终画面的元素。
渲染管线的种类
根据渲染管线的类型,OpenGLES可以被分为两类:
固定管线:
固定渲染管线的OpenGLES不需要也不允许你自己去定义顶点渲染和像素渲染的具体逻辑,它内部已经固化了一套完整的渲染流程,只需要开发者在CPU代码端输入渲染所需要的参数并指定特定的开关,就能完成不同的渲染。OpenGLES 1.x版本就是固定渲染管线的版本。
可编程管线:
可编程渲染管线的OpenGLES版本必须由开发者自行实现渲染流程,否则无法绘制出最终的画面。开发者可以根据自己的具体需要来编写顶点渲染和像素渲染中的具体逻辑,可最大程度的简化渲染管线的逻辑以提高渲染效率,也可自己实现特定的算法和逻辑来渲染出固定管线无法渲染的效果。具有很高的可定制性,但同时也对开发者提出了更高的要求。OpenGLES 2.0及其以上的版本则为可编程渲染管线的版本。
左图是固定渲染管线的流程图,OpenGLES内部从取到数据开始,会经历下列的步骤最终形成可显示的画面。图中橘色的部分在可编程渲染管线中会被替换成可编程的顶点渲染部分(Vertex Shader)和像素渲染部分(Fragment Shader)。
右面OpenGLES管线流程图中展示的橘色部分就是必须由开发者自行编程的部分,顶点渲染部分(VertexShader)用于处理模型的形状给后续的步骤输出一个用于填充色彩的像素区域,像素渲染部分(Fragment Shader)用于在模型的每个面上(经过顶点渲染处理过的面)逐像素填充色彩。
GLSL
OpenGL着色语言(OpenGL Shading Language)是用来在OpenGL中着色编程的语言,也即开发人员写的短小的自定义程序,他们是在图形卡的GPU (Graphic Processor Unit图形处理单元)上执行的,代替了固定的渲染管线的一部分,使渲染管线中不同层次具有可编程性。比如:视图转换、投影转换等。GLSL(GL Shading Language)的着色器代码分成2个部分:Vertex Shader(顶点着色器)和Fragment(片元着色器),有时还会有Geometry Shader(几何着色器)。负责运行顶点着色的是顶点着色器。它可以得到当前OpenGL 中的状态,GLSL内置变量进行传递。GLSL其使用C语言作为基础高阶着色语言,避免了使用汇编语言或硬件规格语言的复杂性。
EAGL
在 iOS中使用EAGL提供的EAGLContext类 来实现和提供一个呈现环境,用来保持OpenGL ES使用到的硬件状态。 EAGL是一个Objective-C API,提供使OpenGL ES与Core Animation和UIKIT集成的接口。
在调用任何OpenGL ES 功能之前必须首先初始化一个EAGLContext 对象。每一个IOS应用的每一个线程都有一个当前context,在调用OpenGL ES函数时,使用或改变此context中的状态。
EAGLContext 的类方法setCurrentContext: 用来设置当前线程的当前context。EAGLContext 的类方法currentContext 返回当前线程的当前context。在切换相同线程的两个上下文之前,必须调用glFlush函数来确保先前已提交的命令被提交到图形硬件中。
GLKit
可以采用不同的方式使用OpenGL ES以便呈现OpenGL ES内容到不同的目标:GLKit和CAEAGLLayer。
为了创建全屏幕的视图或使OpenGL ES内容与UIKit视图集成,可以使用GLKit。在使用GLKit时,GLKit提供的类GLKView类本身实现呈现目标及创建和维护一个framebuffer。
GLKit是一组Objective-C 类,为使用OpenGL ES 提供一个面向对象接口,用来简化OpenGL ES应用的开发。
CAEAGLLayer
为了使OpenGL ES内容作为一个Core Animation层的部分内容时,可以使用CAEAGLLayer 作为呈现目标,并需要另外创建framebuffer以及自己实现和控制整个绘制流程。
GLKit支持四个3D应用开发的关键领域:
1) GLKView 和GLKViewController类提供一个标准的OpenGL ES视图和相关联的呈现循环。GLKView可以作为OpenGL ES内容的呈现目标,GLKViewController提供内容呈现的控制和动画。视图管理和维护一个framebuffer,应用只需在framebuffer进行绘画即可。
2)GLKTextureLoader 为应用提供从IOS支持的各种图像格式的源自动加载纹理图像到OpenGL ES 图像环境的方式,并能够进行适当的转换,并支持同步和异步加载方式。
3)数学运算库,提供向量、矩阵、四元数的实现和矩阵堆栈操作等OpenGL ES 1.1功能。
4)Effect效果类提供标准的公共着色效果的实现。能够配置效果和相关的顶点数据,然后创建和加载适当的着色器。GLKit 包括三个可配置着色效果类:GLKBaseEffect实现OpenGL ES 1.1规范中的关键的灯光和材料模式, GLKSkyboxEffect提供一个skybox效果的实现, GLKReflectionMapEffect 在GLKBaseEffect基础上包括反射映射支持。
苹果最新力推的图形框架 -- Metal
Metal框架支持GPU硬件加速、高级3D图形渲染以及大数据并行运算。且提供了先进而精简的API来确保框架的细粒度(fine-grain),并且在组织架构、程序处理、图形呈现、运算指令以及指令相关数据资源的管理上都支持底层控制。其核心目的是尽可能的减少CPU开销,而将运行时产生的大部分负载交由GPU承担。
Metal相对OpenGL ES拥有更好的性能表现。
Metal提供给开发者对任务采用GPU进行处理的能力。
二、CPU与GPU
图中 ALU代表计算单元,Control代表控制单元,Cache代表高速缓存,DRAM代表内存。CPU芯片空间的5%是ALU,而GPU空间的40%是ALU。这也是导致GPU计算能力超强的原因。
CPU 和 GPU 其设计目标就是不同的,它们分别针对了两种不同的应用场景。CPU 是运算核心与控制核心,需要有很强的运算通用性,兼容各种数据类型,同时也需要能处理大量不同的跳转、中断等指令,因此 CPU 的内部结构更为复杂。而 GPU 则面对的是类型统一、更加单纯的运算,也不需要处理复杂的指令,但也肩负着更大的运算任务。
CPU适合复杂的串行计算,GPU适合于大规模的并行计算。因此图像编解码也常用GPU来进行。
iOS的普通开发者无法直接调用GPU相关api,一般是通过openGL ES/Metal来间接调用gpu来完成任务。
GPU是显卡的芯片,设备可以没有GPU,但不能没有显卡(CPU集成显卡)。CPU集成显卡依靠CPU进行渲染运算。
GPU能干的事情比较单一:接收提交的纹理(Texture)和顶点描述(三角形),应用变换(transform)、混合(合成)并渲染,然后输出到屏幕上。通常你所能看到的内容,主要也就是纹理(图片)和形状(三角模拟的矢量图形)两类。所有的形状都是由三角形模拟而成,包括圆形。(CALayer in CA is two triangles.)
三、图像渲染流水线
图像渲染流程粗粒度地大概分为下面这些步骤:
上述图像渲染流水线中,除了第一部分 Application 阶段,后续主要都由 GPU 负责,下面是 GPU 的渲染流程图:
上图就是一个三角形被渲染的过程中,GPU 所负责的渲染流水线。
Application 应用处理阶段:得到图元
这个阶段具体指的就是图像在应用中被处理的阶段,此时还处于 CPU 负责的时期。在这个阶段应用可能会对图像进行一系列的操作或者改变,最终将新的图像信息传给下一阶段。这部分信息被叫做图元(primitives),通常是三角形、线段、顶点等。
在openGL里面所有的图形都是由图元构成,如下图:
Geometry 几何处理阶段:处理图元
进入这个阶段之后,以及之后的阶段,就都主要由 GPU 负责了。此时 GPU 可以拿到上一个阶段传递下来的图元信息,GPU 会对这部分图元进行处理,之后输出新的图元。这一系列阶段包括:
- 顶点着色器(Vertex Shader):这个阶段中会将图元中的顶点信息进行视角转换、添加光照信息、增加纹理等操作。
- 形状装配(Shape Assembly):图元中的三角形、线段、点分别对应三个 Vertex、两个 Vertex、一个 Vertex。这个阶段会将 Vertex 连接成相对应的形状。
- 几何着色器(Geometry Shader):额外添加额外的Vertex,将原始图元转换成新图元,以构建一个不一样的模型。简单来说就是基于通过三角形、线段和点构建更复杂的几何图形。
Rasterization 光栅化阶段:图元转换为像素
光栅化的主要目的是将几何渲染之后的图元信息,转换为一系列的像素,以便后续显示在屏幕上。这个阶段中会根据图元信息,计算出每个图元所覆盖的像素信息等,从而将像素划分成不同的部分。
一种简单的划分就是根据中心点,如果像素的中心点在图元内部,那么这个像素就属于这个图元。如上图所示,深蓝色的线就是图元信息所构建出的三角形;而通过是否覆盖中心点,可以遍历出所有属于该图元的所有像素,即浅蓝色部分。
Pixel 像素处理阶段:处理像素,得到位图
经过上述光栅化阶段,我们得到了图元所对应的像素,此时,我们需要给这些像素填充颜色和效果。所以最后这个阶段就是给像素填充正确的内容,最终显示在屏幕上。这些经过处理、蕴含大量信息的像素点集合,被称作位图(bitmap)。也就是说,Pixel 阶段最终输出的结果就是位图,过程具体包含:
这些点可以进行不同的排列和染色以构成图样。当放大位图时,可以看见赖以构成整个图像的无数单个方块。只要有足够多的不同色彩的像素,就可以制作出色彩丰富的图象,逼真地表现自然界的景象。缩放和旋转容易失真,同时文件容量较大。
- 片段着色器(Fragment Shader):也叫做 Pixel Shader,这个阶段的目的是给每一个像素 Pixel 赋予正确的颜色。颜色的来源就是之前得到的顶点、纹理、光照等信息。由于需要处理纹理、光照等复杂信息,所以这通常是整个系统的性能瓶颈。
- 测试与混合(Tests and Blending):也叫做 Merging 阶段,这个阶段主要处理片段的前后位置以及透明度。这个阶段会检测各个着色片段的深度值 z 坐标,从而判断片段的前后位置,以及是否应该被舍弃。同时也会计算相应的透明度 alpha 值,从而进行片段的混合,得到最终的颜色。
在图像渲染流程结束之后,接下来就需要将得到的像素信息显示在物理屏幕上了。GPU 最后一步渲染结束之后像素信息,被存在帧缓冲器(Framebuffer)中,之后视频控制器(Video Controller)会读取帧缓冲器中的信息,经过数模转换传递给显示器(Monitor),进行显示。完整的流程如下图所示:
经过 GPU 处理之后的像素集合,也就是位图,会被帧缓冲器缓存起来,供之后的显示使用。显示器的电子束会从屏幕的左上角开始逐行扫描,屏幕上的每个点的图像信息都从帧缓冲器中的位图进行读取,在屏幕上对应地显示。扫描的流程如下图所示:
四、vsync
vsync意思是垂直同步信号。因为扫描以每一行(hsync)为单位,vsync也叫帧同步信号,一帧也就是显示的一个画面。
显示器的电子枪从上到下一行行扫描,扫描完成后显示器就呈现一帧画面,随后电子枪回到初始位置继续下一次扫描。显示器会用硬件时钟产生一系列的定时信号(硬件时钟是存储在主板上CMOS里的时钟,不会受到性能影响出现跑偏,其频率等于屏幕设置的刷新率)。当电子枪换到新的一行,准备进行扫描时,显示器会发出一个水平同步信号(horizonal synchronization),简称 HSync;而当一帧画面绘制完成后,电子枪回复到原位,准备画下一帧前,显示器会发出一个垂直同步信号(vertical synchronization),简称 VSync。显示器通常以固定频率进行刷新,这个刷新率就是 VSync 信号产生的频率。
帧率与刷新率
帧率:单位 fps,是指 gpu 生成帧的速率。
刷新率:单位赫兹/Hz,是指设备刷新屏幕的频率,该值对于特定的设备来说是个常量,如 60hz。
在单帧缓存的情况下,帧率与刷新率的不一致容易造成撕裂效果。(显示出来的图像就会出现上半部分和下半部分明显偏差的现象)
双缓冲的模型下,工作流程这样的:
两个缓存区分别为 Back Buffer 和 Frame Buffer。GPU 向 Back Buffer 中写数据,屏幕从 Frame Buffer 中读数据。VSync 信号负责调度从 Back Buffer 到 Frame Buffer 的复制(指针互换)操作,可认为该复制操作在瞬间完成。在某个时间点,一个屏幕刷新周期完成,进入短暂的刷新空白期。此时,VSync 信号产生,先完成复制(指针互换)操作,然后通知 CPU/GPU 绘制下一帧图像。并将Back Buffer 和 Frame Buffer指针交换,最后将 Frame Buffer 的数据显示到屏幕上。
在这种模型下,只有当 VSync 信号产生时,CPU/GPU 才会开始绘制。这样,当帧率大于刷新频率时,帧率就会被迫跟刷新频率保持同步,从而避免“tearing”现象。
注意,当 VSync 信号发出时,如果 GPU/CPU 正在生产帧数据,此时不会发生交换操作。屏幕进入下一个刷新周期时,从 Frame Buffer 中取出的是“老”数据,而非正在产生的帧数据,即两个刷新周期显示的是同一帧数据。这是我们称发生了“掉帧”(Dropped Frame,Skipped Frame,Jank)现象。
这里的双缓存指双缓存+VSync,如果只是单纯的双缓存仍然会造成“tearing”现象(当视频控制器还未读取完成时,即屏幕内容刚显示一半时,GPU 将新的一帧内容提交到帧缓冲区并把两个缓冲区进行交换后,视频控制器就会把新的一帧数据的下半段显示到屏幕上,造成画面撕裂现象),VSync在这里不止负责调度屏幕显示(帧缓冲区的交换及显示),也负责调度CPU/GPU 才会开始绘制。这样能解决画面撕裂现象,也增加了画面流畅度,但需要消费更多的计算资源,也会带来部分延迟。现在iOS 设备会始终使用双缓存,并开启垂直同步。
卡顿产生的原因
事实上CPU和GPU的处理占用了两个VSync,而不是一个
在 VSync 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 App,App 主线程开始在 CPU 中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等。随后 CPU 会将计算好的内容提交到 GPU 去,由 GPU 进行变换、合成、渲染。随后 GPU 会把渲染结果提交到帧缓冲区去,等待下一次 VSync 信号到来时显示到屏幕上。由于垂直同步的机制,如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。
上图第三次VSync信号出现掉帧情况,此时就不会触发CADisplayLink的回调,因此可以用CADisplayLink用来监测屏幕掉帧率。
要在屏幕上显示视图,需要CPU和GPU一起协作,CPU计算好显示的内容提交到GPU,GPU渲染完成后将结果放到帧缓存区,随后视频控制器会按照 VSync 信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示。
VRAM指显卡所使用的内存,简称显存,亦称帧缓存,是用来存储显示芯片处理过或者即将读取的渲染数据。
iOS使用的是双缓冲机制。即GPU会预先渲染好一帧放入一个缓冲区内(前帧缓存),让视频控制器读取,当下一帧渲染好后,GPU会直接把视频控制器的指针指向第二个缓冲器(后帧缓存)。当你视频控制器已经读完一帧,准备读下一帧的时候,GPU会等待显示器的VSync信号发出后,前帧缓存和后帧缓存会瞬间切换,后帧缓存会变成新的前帧缓存,同时旧的前帧缓存会变成新的后帧缓存。
帧缓存:接收渲染结果的缓冲区,为GPU指定存储渲染结果的区域
帧缓存可以同时存在多个,但是屏幕显示像素受到保存在前帧缓存(front frame buffer)的特定帧缓存中的像素颜色元素的控制。
程序的渲染结果通常保存在后帧缓存(back frame buffer)在内的其他帧缓存,当渲染后的后帧缓存完成后,前后帧缓存会互换。(这部分操作由操作系统来完成)
前帧缓存决定了屏幕上显示的像素颜色,会在适当的时候与后帧缓存切换。
Core Animation的合成器会联合OpenGL ES层和UIView层、StatusBar层等,在后帧缓存混合产生最终的颜色,并切换前后帧缓存;
27 : <CFRunLoopObserver 0x600003af0460 [0x10f229bf0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2001000, callout = _afterCACommitHandler (0x1372fa48d), context = <CFRunLoopObserver context 0x7fcff5305370>}
31 : <CFRunLoopObserver 0x600003af03c0 [0x10f229bf0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 1999000, callout = _beforeCACommitHandler (0x1372fa483), context = <CFRunLoopObserver context 0x7fcff5305370>}
32 : <CFRunLoopObserver 0x600003af0a00 [0x10f229bf0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2000000, callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv (0x112674f54), context = <CFRunLoopObserver context 0x0>}
主线程的Runloop注册了3个observer,一个优先级为1999000,Activity为BeforeWaiting | Exit,回调为_beforeCACommitHandler。
另一个优先级为2001000,Activity也为BeforeWaiting | Exit,回调为_afterCACommitHandler。
还有一个observer,优先级为2000000,Activity为BeforeWaiting | Exit,回调为CA::Transaction::observer_callback(__CFRunLoopObserver, unsigned long, void),这个是用来更新内容和布局的。
当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。
从以下的drawRect调用堆栈可以看出,主线程Runloop在准备进入休眠前,处理CoreAnimation的全局容器中的事务,并更新 UI 界面。
thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 30.1
frame #0: 0x000000010d0dc191 FinDemo-[FATCanvas2DParser parse2DActions:forContext:rect:scaleCanvas:](self=0x0000600000536af0, _cmd="parse2DActions:forContext:rect:scaleCanvas:", actions=@"741 elements", context=0x0000600003ef3a80, rect=(origin = (x = 0, y = 0), size = (width = 375, height = 718)), scaleCanvas=YES) at FATCanvas2DParser.m:58:5 frame #1: 0x000000010d0f09c6 FinDemo
-[FATCanvas2DView drawRect:](self=0x00007fcff5275920, cmd="drawRect:", rect=(origin = (x = 0, y = 0), size = (width = 375, height = 718))) at FATCanvas2DView.m:42:9
frame #2: 0x0000000137887afb UIKitCore-[UIView(CALayerDelegate) drawLayer:inContext:] + 563 frame #3: 0x000000011259541f QuartzCore
CABackingStoreUpdate + 219
frame #4: 0x00000001126f7415 QuartzCoreinvocation function for block in CA::Layer::display_() + 53 frame #5: 0x00000001126edb1a QuartzCore
-[CALayer _display] + 2188
frame #6: 0x0000000112700691 QuartzCoreCA::Layer::layout_and_display_if_needed(CA::Transaction*) + 481 frame #7: 0x000000011263c0ca QuartzCore
CA::Context::commit_transaction(CA::Transaction, double, double) + 652
frame #8: 0x0000000112673c47 QuartzCoreCA::Transaction::commit() + 699 frame #9: 0x0000000112674fc8 QuartzCore
CA::Transaction::flush_as_runloop_observer(bool) + 60
frame #10: 0x000000010eea5c77 CoreFoundation__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 23 frame #11: 0x000000010eea049c CoreFoundation
__CFRunLoopDoObservers + 541
frame #12: 0x000000010eea0a4c CoreFoundation__CFRunLoopRun + 1126 frame #13: 0x000000010eea0103 CoreFoundation
CFRunLoopRunSpecific + 567
frame #14: 0x000000011939fcd3 GraphicsServicesGSEventRunModal + 139 frame #15: 0x00000001372c7e63 UIKitCore
-[UIApplication _run] + 928
frame #16: 0x00000001372cca53 UIKitCoreUIApplicationMain + 101 frame #17: 0x000000010cfcc626 FinDemo
main(argc=1, argv=0x00007ffee2c35cb0) at main.m:17:12
frame #18: 0x000000010d7d5e1e dyld_sim`start_sim + 10
五、iOS渲染流程
iOS 的显示系统是由 VSync 信号驱动的,VSync 信号由硬件时钟生成,每秒钟发出 60 次(这个值取决设备硬件)。iOS 图形服务接收到 VSync 信号后,会通过 IPC(进程间通信CFMachPort) 通知到 App 内。App 的 Runloop 在启动后会注册对应的 CFRunLoopSource 通过 mach_port 接收传过来的时钟信号通知,随后 Source 的回调会驱动整个 App 的动画与显示。
Core Animation 在 RunLoop 中注册了一个 Observer,监听了 BeforeWaiting 和 Exit 事件。这个 Observer 的优先级是 2000000,低于常见的其他 Observer。当一个触摸事件到来时,RunLoop 被唤醒,App 中的代码会执行一些操作,比如创建和调整视图层级、设置 UIView 的 frame、修改 CALayer 的透明度、为视图添加一个动画;这些操作最终都会被 CALayer 捕获,并通过 CATransaction 提交到一个中间状态去。当上面所有操作结束后,RunLoop 即将进入休眠(或者退出)时,关注该事件的 Observer 都会得到通知。这时 CA 注册的那个 Observer 就会在回调中,把所有的中间状态合并提交到 GPU 去显示;如果此处有动画,通过 DisplayLink 稳定的刷新机制会不断的唤醒runloop,使得不断的有机会触发observer回调,从而根据时间来不断更新这个动画的属性值并绘制出来。
动画应该是通过渲染服务来实现,后文有具体分析。
如下图:
CoreAnimation提交事务,CA::Transaction::commit(),包括自己和子树(view hierarchy)的layout状态等;
RenderServer渲染服务层解析提交的子树状态,生成绘制指令;
GPU执行绘制指令;
显示渲染后的数据;
我们看到在应用程序(Application)和渲染服务器(Render Server)中都有 Core Animation ,但是渲染工作并不是在应用程序里(尽管它有 Core Animation)完成的。
Render Server.App是iOS系统独立负责渲染的进程。
通过 IPC 将视图层级及相关数据提交给 Render Server。Render Server 处理完数据后,再传递至 GPU。最后由 GPU 调用 iOS 的图像设备进行显示。
Core Animation 流水线的详细过程如下:
首先,由 app 处理事件(Handle Events),如:用户的点击操作,在此过程中 app 可能需要更新 视图树,相应地,图层树 也会被更新。
其次,app 通过 CPU 完成对显示内容的计算(Commit Transaction),如:视图的创建、布局计算、图片解码、文本绘制等。在完成对显示内容的计算之后,app 对图层进行打包,并在下一次 RunLoop 时将其发送至 Render Server,即完成了一次 Commit Transaction 操作。从图中看出是直接交给了RenderServer进行decode,并在下一个runloop进行调用gpu。
Render Server 主要执行 Open GL、Core Graphics 相关程序,并调用 GPU
GPU 则在物理层上完成了对图像的渲染。
最终,GPU 通过 Frame Buffer、视频控制器等相关部件,将图像显示在屏幕上。
Decode
打包好的图层被传输到 Render Server 之后,首先会进行解码。Core Animation对渲染树中的每个可见图层通过OpenGL循环转换成纹理三角板。注意完成解码之后需要等待下一个 RunLoop 才会执行将具体操作绘制(OpenGL/Metal的操作)转发给下个流程。Draw Calls
解码完成后,渲染服务器必须等待下一次重新同步。Core Animation 会调用下层渲染框架( OpenGL 或者 Metal)的方法进行顶点着色器、图元装配、光栅化、片元着色器、混合等渲染工作,进而调用到 GPU。iOS 上是双缓冲机制,所以这个信号表示上一个 buffer 已经扫描展示,显示器已经切换到了另一个 buffer。当下,需要绘制新的位图并更新到上一帧所在的 buffer 上。Render
这一阶段主要由 GPU 进行渲染。GPU 的绘制一般都是直接绘制在 buffer 上,GPU 的离屏渲染大概是开辟一块内存进行渲染操作,最后将两块内存混合,在两块内存之间进行切换会消耗性能。Display
显示阶段,需要等 render 结束的下一个 RunLoop 触发显示。
对上述步骤进行串联,它们执行所消耗的时间远远超过 16.67 ms,因此为了满足对屏幕的 60 FPS 刷新率的支持,需要将这些步骤进行分解,通过流水线的方式进行并行执行,如下图所示。
渲染等待
由于每一帧的顶点和像素处理相对独立,iOS会将CPU处理,顶点处理,像素处理安排在相邻的三帧中。如图,当一个渲染命令提交后,要在当帧之后的第三帧,渲染结果才会显示出来。
Commit Transaction
在 Core Animation 流水线中,app 调用 Render Server 前的最后一步 Commit Transaction 其实可以细分为 4 个步骤:Layout,Display,Prepare,Commit
Layout
Layout 阶段主要进行视图构建,包括:LayoutSubviews 方法的重载,addSubview: 方法填充子视图,以及设置图层属性(位置,背景色,边框)等。
setNeedsLayout()方法
setNeedsLayout()方法的调用可以触发layoutSubviews,调用这个方法代表向系统表示视图的布局需要重新计算。不过调用这个方法只是为当前的视图打了一个脏标记,告知系统需要在下一次run loop中重新布局这个视图。也就是调用setNeedsLayout()后会有一段时间间隔,然后触发layoutSubviews.当然这个间隔不会对用户造成影响,因为永远不会长到对界面造成卡顿。layoutIfNeeded()方法
layoutIfNeeded()方法的作用是告知系统,当前打了脏标记的视图需要立即更新,不要等到下一次run loop到来时在更新,此时该方法会立即触发layoutSubviews方法。当然但如果你调用了layoutIfNeeded之后,并且没有任何操作向系统表明需要刷新视图,那么就不会调用layoutsubview.这个方法在你需要依赖新布局,无法等到下一次 run loop的时候会比setNeedsLayout有用。
Display
Display 阶段主要进行视图绘制,这里仅仅是设置最要成像的图元数据。重载视图的 drawRect: 方法可以自定义 UIView 的显示,其原理是在 drawRect: 方法内部绘制寄宿图,该过程使用 CPU 和内存。
由于重写了 drawRect: 方法,导致绘制过程从 GPU 转移到了 CPU,这就导致了一定的效率损失。与此同时,这个过程会额外使用 CPU 和内存,因此需要高效绘制,否则容易造成 CPU 卡顿或者内存爆炸。
Prepare
Prepare 阶段属于附加步骤,一般处理图像的解码和转换等操作。
解码就是我们常说的将图片解码成 bitmap 格式存储在内存中。
这是Core Animation准备发送动画数据到渲染服务的阶段。这同时也是Core Animation将要执行一些别的事务例如解码动画过程中将要显示的图片的时间点。
PNG或者JPEG压缩之后的图片文件会比同质量的位图小得多。但是在图片绘制到屏幕上之前,必须把它扩展成完整的未解压的尺寸(通常等同于图片宽 x 长 x 4个字节)。为了节省内存,iOS通常直到真正绘制的时候才去解码图片。
Commit
这是最后的阶段,Core Animation打包所有图层和动画属性,然后通过IPC(内部处理通信)发送到渲染服务进行显示。
这些阶段仅仅发生在你的应用程序之内,在动画在屏幕上显示之前仍然有更多的工作。一旦打包的图层和动画到达渲染服务进程,他们会被反序列化来形成另一个叫做渲染树的图层树。
使用这个树状结构,渲染服务对动画的每一帧做出如下工作:
对所有的图层属性计算中间值,设置OpenGL几何形状(纹理化的三角形)来执行渲染
在屏幕上渲染可见的三角形
所以一共有六个阶段;最后两个阶段在动画过程中不停地重复。前五个阶段都在软件层面处理(通过CPU),只有最后一个被GPU执行。而且,你真正只能控制前两个阶段:布局和显示。Core Animation框架在内部处理剩下的事务,你也控制不了它。
GPU为一个具体的任务做了优化:它用来采集图片和形状(三角形),运行变换,应用纹理和混合然后把它们输送到屏幕上。
大多数CALayer的属性都是用GPU来绘制。比如如果你设置图层背景或者边框的颜色,那么这些可以通过着色的三角板实时绘制出来。如果对一个contents属性设置一张图片,然后裁剪它 - 它就会被纹理的三角形绘制出来,而不需要软件层面做任何绘制。
但是有一些事情会降低(基于GPU)图层绘制,比如:
太多的几何结构 - 这发生在需要太多的三角板来做变换,以应对处理器的栅格化的时候。现代iOS设备的图形芯片可以处理几百万个三角板,所以在Core Animation中几何结构并不是GPU的瓶颈所在。但由于图层在显示之前通过IPC发送到渲染服务器的时候(图层实际上是由很多小物体组成的特别重量级的对象),太多的图层就会引起CPU的瓶颈。这就限制了一次展示的图层个数(见本章后续“CPU相关操作”)。
重绘 - 主要由重叠的半透明图层引起。GPU的填充比率(用颜色填充像素的比率)是有限的,所以需要避免重绘(每一帧用相同的像素填充多次)的发生。在现代iOS设备上,GPU都会应对重绘;即使是iPhone 3GS都可以处理高达2.5的重绘比率,并任然保持60帧率的渲染(这意味着你可以绘制一个半的整屏的冗余信息,而不影响性能),并且新设备可以处理更多。
离屏绘制 - 这发生在当不能直接在屏幕上绘制,并且必须绘制到离屏图片的上下文中的时候。离屏绘制发生在基于CPU或者是GPU的渲染,或者是为离屏图片分配额外内存,以及切换绘制上下文,这些都会降低GPU性能。对于特定图层效果的使用,比如圆角,图层遮罩,阴影或者是图层光栅化都会强制Core Animation提前渲染图层的离屏绘制。但这不意味着你需要避免使用这些效果,只是要明白这会带来性能的负面影响。
过大的图片 - 如果视图绘制超出GPU支持的2048x2048或者4096x4096尺寸的纹理,就必须要用CPU在图层每次显示之前对图片预处理,同样也会降低性能。
动画渲染原理
动画的步骤与commit transaction一样,唯一不同是,在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 逐帧进行渲染。
渲染服务
动画和屏幕上组合的图层实际上被一个单独的进程管理,而不是你的应用程序。这个进程就是所谓的渲染服务。在iOS5和之前的版本是SpringBoard进程(同时管理着iOS的主屏)。在iOS6之后的版本中叫做BackBoard。
当图层被成功打包,发送到渲染服务之后,CPU仍然要做如下工作:为了显示 屏幕上的图层,Core Animation必须对渲染树中的每个可见图层通过OpenGL循环 转换成纹理化的三角形(decode)。
纹理的概念:纹理是一个用来保存图像的颜色元素值的 OpenGL ES 缓存,可以简单理解为一个单位。
生成帧缓存(CPU执行):渲染服务首先将图层数据交给OpenGL ES进行纹理生成和着色,生成前后帧缓存。再根据显示硬件的刷新频率,一般以设备的VSync信号和CADisplayLink为标准,进行前后帧缓存的切换。渲染(GPU执行) :将最终要显示在画面上的后帧缓存交给GPU,进行采集图片和形状,运行变换,应用纹理和混合,最终显示在屏幕上。
简单来说,OpenGL ES是对图层进行取色,采样,生成纹理,绑定数据,生成前后帧缓存。
1)生成(Generate)— 请 OpenGL ES 为图形处理器制的缓存生成一个独一无二的标识符。
2)绑定(Bind)— 告诉 OpenGL ES 为接下来的运算使用一个缓存。
3)缓存数据(Buffer Data)— 让 OpenGL ES 为当前定的缓存分配并初始化 够的内存(通常是从 CPU 制的内存复制数据到分配的内存)。
4)启用(Enable)或者(Disable)— 告诉 OpenGL ES 在接下来的渲染中是 使用缓存中的数据。
5)设置指针(SetPointers)— 告诉 Open-GL ES 在缓存中的数据的类型和所有需 要的数据的内存移值。
6)绘图(Draw) — 告诉 OpenGL ES 使用当前定并启用的缓存中的数据渲染 整个场景或者某个场景的一部分。
7)删除(Delete)— 告诉 OpenGL ES 除以前生成的缓存并释相关的资源。
最终,生成前后帧缓存会再交由GPU进行最后一步的工作。
以上步骤在CPU执行应该是原作者理解有误,这些步骤应该是GPU渲染阶段的步骤。在渲染步骤之前,进行decode并等待下一个runloop进行draw calls。decode阶段做的事情是将图层树转换成纹理及三角形(图元)。draw calls则通过调用openGL ES指令来操作GPU开始渲染。后文Command Buffer会详细说明渲染流程。
GPU会根据生成的前后帧缓存数据,根据实际情况进行合成,其中造成GPU渲染负担的一般是:离屏渲染,图层混合,延迟加载。
CRT屏幕成像
对于CRT来讲,屏幕上的图形图像是由一个个因电子束击打而发光的荧光点组成,由于显像管内荧光粉受到电子束击打后发光的时间很短,所以电子束必须不断击打荧光粉使其持续发光。电子枪从屏幕的左上角的第一行(行的多少根据显示器当时的分辨率所决定,比如800X600分辨率下,电子枪就要扫描600行)开始,从左至右逐行扫描,第一行扫描完后再从第二行的最左端开始至第二行的最右端,一直到扫描完整个屏幕后再从屏幕的左上角开始,这时就完成了一次对屏幕的刷新。
CRT 的电子枪从上到下一行行扫描,扫描完成后显示器就呈现一帧画面,随后电子枪回到初始位置继续下一次扫描。为了把显示器的显示过程和系统的视频控制器进行同步,显示器(或者其他硬件)会用硬件时钟产生一系列的定时信号。当电子枪换到新的一行,准备进行扫描时,显示器会发出一个水平同步信号(horizonal synchronization),简称 HSync;而当一帧画面绘制完成后,电子枪回复到原位,准备画下一帧前,显示器会发出一个垂直同步信号(vertical synchronization),简称 VSync。显示器通常以固定频率进行刷新,这个刷新率就是 VSync 信号产生的频率。
六、离屏渲染
通常的渲染流程是这样的:
App 通过 CPU 和 GPU 的合作,不停地将内容渲染完成放入 Framebuffer 帧缓冲器中,而显示屏幕不断地从 Framebuffer 中获取内容,显示实时的内容。
而离屏渲染的流程是这样的:
与普通情况下 GPU 直接将渲染好的内容放入 Framebuffer 中不同,需要先额外创建离屏渲染缓冲区 Offscreen Buffer,将提前渲染好的内容放入其中,等到合适的时机再将 Offscreen Buffer 中的内容进一步叠加、渲染,完成后将结果切换到 Framebuffer 中。
离屏渲染指的是在图像在绘制到当前屏幕前,需要先进行一次渲染,之后才绘制到当前屏幕。
OpenGL中,GPU屏幕渲染有以下两种方式:
On-Screen Rendering即当前屏幕渲染,指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行。
Off-Screen Rendering即离屏渲染,指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。
为什么离屏渲染会发生卡顿?主要包括两方面内容:
创建新的缓冲区。
上下文切换,离屏渲染的整个过程,需要多次切换上下文环境,先是从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上又需要将上下文环境从离屏切换到当前屏幕。而上下文环境的切换是要付出很大代价的。(GPU非常擅长大规模并行计算,但是我想频繁的上下文切换显然不在其设计考量之中)对于显卡来说,onscreen到offscreen的上下文环境切换是非常昂贵的(涉及到OpenGL的pipelines和barrier等)。Offscreen Render需要更多的渲染通道,而且不同的渲染通道间切换需要耗费一定的时间,这个时间内GPU会闲置,当通道达到一定数量,对性能也会有较大的影响;
如果shouldRasterize被设置成YES,在触发离屏绘制的同时,会将光栅化后的内容缓存起来,如果对应的layer及其sublayers没有发生改变,在下一帧的时候可以直接复用。这将在很大程度上提升渲染性能。
如果将不在GPU的当前屏幕缓冲区中进行的渲染都称为离屏渲染,那么就还有另一种特殊的“离屏渲染”方式:CPU渲染。
如果我们重写了drawRect方法,并且使用任何Core Graphics的技术进行了绘制操作,就涉及到了CPU渲染。整个渲染过程由CPU在App内同步地完成,渲染得到的bitmap最后再交由GPU用于显示。
相对于切换上下文,GPU更擅长渲染。离屏渲染会导致GPU利用率不到100%,帧率却很低。(切换上下文会产生idle time)
In particular, a few (implementing drawRect and doing any CoreGraphics drawing, drawing with CoreText [which is just using CoreGraphics]) are indeed “offscreen drawing,” but they’re not what we usually mean when we say that. They’re very different from the rest of the list. When you implement drawRect or draw with CoreGraphics, you’re using the CPU to draw, and that drawing will happen synchronously within your application. You’re just calling some function which writes bits in a bitmap buffer, basically.
The other forms of offscreen drawing happen on your behalf in the render server (a separate process) and are performed via the GPU (not via the CPU, as suggested in the previous paragraph). When the OpenGL renderer goes to draw each layer, it may have to stop for some subhierarchies and composite them into a single buffer. You’d think the GPU would always be faster than the CPU at this sort of thing, but there are some tricky considerations here. It’s expensive for the GPU to switch contexts from on-screen to off-screen drawing (it must flush its pipelines and barrier), so for simple drawing operations, the setup cost may be greater than the total cost of doing the drawing in CPU via e.g. CoreGraphics would have been. So if you’re trying to deal with a complex hierarchy and are deciding whether it’s better to use –[CALayer setShouldRasterize:] or to draw a hierarchy’s contents via CG, the only way to know is to test and measure.
You could certainly end up doing two off-screen passes if you draw via CG within your app and display that image in a layer which requires offscreen rendering. For instance, if you take a screenshot via –[CALayer renderInContext:] and then put that screenshot in a layer with a shadow.
Also: the considerations for shouldRasterize are very different from masking, shadows, edge antialiasing, and group opacity. If any of the latter are triggered, there’s no caching, and offscreen drawing will happen on every frame; rasterization does indeed require an offscreen drawing pass, but so long as the rasterized layer’s sublayers aren’t changing, that rasterization will be cached and repeated on each frame. And of course, if you’re using drawRect: or drawing yourself via CG, you’re probably caching locally.
Speaking of caching: if you’re doing a lot of this kind of drawing all over your application, you may need to implement cache-purging behavior for all these (probably large) images you’re going to have sitting around on your application’s heap. If you get a low memory warning, and some of these images are not actively being used, it may be best for you to get rid of those stretchable images you drew (and lazily regenerate them when needed). But that may end up just making things worse, so testing is required there too.
这段话的主要意思是,渲染分为三种,当前屏幕渲染,离屏渲染(GPU),CPU渲染(一种特殊的离屏渲染)。离屏渲染(GPU)并不一定性能比CPU渲染好。开启shouldRasterize,光栅化将被缓存并在每一帧上重复,直到layer被修改;否则不会缓存,并且会在每一帧上进行离屏绘制。过多的缓存可能会带来内存问题。
七、CALayer
寄宿图
视图层级拥有 视图树 的树形结构,对应 CALayer 层级也拥有 图层树 的树形结构。视图树中添加或者删除视图,相对应的图层树也会添加或者删除图层保证视图树和图层树一一对应。
CALayer 中包含一个 contents 属性指向一块缓存区,称为 backing store,可以存放位图(Bitmap)。iOS 中将该缓存区保存的图片称为 寄宿图。
在图形渲染中有两种方式一是顶点进行绘制,二是通过纹理。在绘制图形中也有两种方式,一是手动绘制(对应图形渲染中的顶点绘制),二是使用图片(图形渲染中的通过纹理)。
实现寄宿图的两种方式:
- Contents Image 是指通过 CALayer 的 contents 属性来配置图片。contents 属性指向的一块缓存区域,称为 backing store,可以存放 bitmap 数据。
- Custom Drawing是使用Core Graphics 直接绘制寄宿图。在开发中我们一般是通过实现UIView的- (void)drawRect:完成绘制的。
-drawRect: 是一个 UIView 方法,但事实上都是底层的 CALayer 完成了重绘工作并保存了产生的图片。下图所示为 -drawRect: 绘制定义寄宿图的基本原理。
-drawRect:的调用是在视图出现在屏幕的时候,-drawRect:利用其封装的代码通过CoreGraphics来绘制一个寄宿图,当寄宿图绘制结束后会被缓存到backing store,直到它需要更新的时候(通过调用-setNeedsDisplay)去重新绘制更新。
即使重写了-drawRect:方法什么也不做,-drawRect :方法就会自动被调用,并在调用之前创建了一个合适尺寸的空寄宿图(尺寸由bounds和contentsScale决定)和一个Core Graphics的绘制上下文环境,为绘制寄宿图做准备,它作为ctx参数传入。在这一步生成的空寄宿图内存是相当巨大的,因此尽量避免重写-drawRect:方法。
当渲染系统准备就绪,调用视图的-display方法,同时装配像素存储空间,建立一个CoreGraphics上下文(CGContextRef),将上下文push进上下文堆栈,绘图程序进入对应的内存存储空间。
drawRect:如果开发者重写了这个方法就会在CPU中将layer通过Core Graphics直接处理成bitmap,就不会在通过GPU来完成bitmap的渲染。Core Animation必须创建一个内存中等大小的寄宿图片。然后一旦绘制结束之后, 必须把图片数据通过IPC传到渲染服务。
视图在屏幕上出现的时候-drawRect:会自动被调用。-drawRect:方法里面的代码利用Core Graphics绘制一个寄宿图,然后被缓存起来直到需要被更新(一般是调用了- setNeedDisplay方法)。
因此,CoreGraphics使用-drawRect:进行绘制,相比openGL ES,会先由CPU渲染成寄宿图再交给GPU,相对openGL ES,更多的工作交给了CPU。
当显示一个UIImageView时,Core Animation会创建一个OpenGL ES纹理,并确保在这个图层中的位图被上传到对应的纹理中。当你重写-drawInContext方法时,Core Animation会请求分配一个纹理,同时确保Core Graphics会将你在-drawInContext中绘制的东西放入到纹理的位图数据中。
渲染的过程如下:
- UIView的layer层有一个content,指向一块缓存,即backing store
- UIView绘制时,会调用drawRect方法,通过context将数据写入backing store
- 在backing store写完后,通过render server交给GPU去渲染,将backing store中的bitmap数据显示在屏幕上。
异步绘制
- 在当前run loop 将要结束的时候,才会调用CALayer的display方法进入到真正的绘制当中
- 在CALayer的display方法中,会判断layer的代理方法displayLayer:是否被实现,如果代理没有实现这个方法,则进入系统绘制流程,否则进入异步绘制入口。
/* If defined, called by the default implementation of the -display
method, in which case it should implement the entire display
process (typically by setting the `contents' property). */
(void)displayLayer:(CALayer *)layer;
系统绘制的最后CALayer把生成的bitmap位图传给GPU去渲染。
在displayLayer:方法中我们开辟子线程,最后回到主线程,把Image图片赋值给layer的contents属性。
异步绘制可参考YYAsyncLayer。
八、隐式绘制(寄宿图)
文本
CATextLayer和UILabel都是直接将文本绘制在图层的寄宿图中。这两种方法都用了软件的方式绘制,因此他们实际上要比硬件加速合成方式要慢。
不论如何,尽可能地避免改变那些包含文本的视图的frame,因为这样做的话文本就需要重绘。例如,如果你想在图层的角落里显示一段静态的文本,但是这个图层经常改动,你就应该把文本放在一个子图层中。
layer.shouldRasterize光栅化
启用shouldRasterize属性会将图层绘制到一个屏幕之外的图像。然后这个图像将会被缓存起来并绘制到实际图层的contents和子图层。如果有很多的子图层或者有复杂的效果应用,这样做就会比重绘所有事务的所有帧划得来得多。但是光栅化原始图像需要时间,而且还会消耗额外的内存。
开启光栅化后,会触发离屏渲染,Render Server 会强制将 CALayer (包括其子layer,以及圆角、阴影、group opacity等等)的渲染位图结果 bitmap 保存下来,这样下次再需要渲染时就可以直接复用,而不会再次触发离屏渲染,从而提高效率。
而保存的 bitmap 包含 layer 的 subLayer、圆角、阴影、组透明度 group opacity 等,所以如果 layer 的构成包含上述几种元素,结构复杂且需要反复利用,那么就可以考虑打开光栅化。
光栅化是把GPU的操作转到CPU上,生成bitmap位图缓存,直接读取复用。
CALayer会被光栅化为bitmap,shadows、cornerRadius等效果会被缓存。
shouldRasterize 适合静态页面显示,动态页面会增加开销。
如果光栅化的元素100ms没有被使用将被移除。
九、模型树→呈现树→渲染树
UIView 的 Layer 在系统内部,被维护着三份同样的树形数据结构(图层树),分别是:
模型树(默认的图层树)DisplayLink(这里是代码可以操纵的,设置属性的最终值会立刻在这里更新);
呈现树(是一个中间层,系统就在这一层上更改属性,进行各种渲染操作。比如一个动画是更改alpha值从0到1,那么在逻辑树上此属性会被立刻更新为最终属性1,而在动画树上会根据设置的动画时间从0逐步变化到1);
渲染树(其属性值就是当前正被显示在屏幕上的属性值)
CoreAnimation作为一个复合引擎,将不同的视图层组合在屏幕中,并且存储在图层树中,向我们展示了所有屏幕上的一切。
整个过程其实经历了三个树状结构,才显示到了屏幕上:模型树-->呈现树-->渲染树,如图:
通常,我们操作的是模型树,在重绘周期最后,我们会将模型树相关内容(层次结构、图层属性和动画)序列化,通过IPC传递给专门负责屏幕渲染的渲染进程。渲染进程拿到数据并反序列化出树状结构–呈现树。这个呈现图层实际上是模型图层的复制,但是它的属性值代表了在任何指定时刻当前外观效果。换句话说,你可以通过呈现图层的值来获取当前屏幕上真正显示出来的值。
CoreAnimation动画,即基于事务的动画,是最常见的动画实现方式。动画执行者是专门负责渲染的渲染进程,操作的是呈现树。我们应该尽量使用CoreAnimation来控制动画,因为CoreAnimation是充分优化过的。
CALayer的属性行为其实很不正常,因为改变一个图层的属性并没有立刻生效,而是通过一段时间渐变更新。这是怎么做到的呢?
当你改变一个图层的属性,属性值的确是立刻更新的(如果你读取它的数据,你会发现它的值在你设置它的那一刻就已经生效了),但是屏幕上并没有马上发生改变。这是因为你设置的属性并没有直接调整图层的外观,相反,他只是定义了图层动画结束之后将要变化的外观。
当设置CALayer的属性,实际上是在定义当前事务结束之后图层如何显示的模型。Core Animation扮演了一个控制器的角色,并且负责根据图层行为和事务设置去不断更新视图的这些属性在屏幕上的状态。
我们讨论的就是一个典型的微型MVC模式。CALayer是一个连接用户界面(就是MVC中的view)虚构的类,但是在界面本身这个场景下,CALayer的行为更像是存储了视图如何显示和动画的数据模型。实际上,在苹果自己的文档中,图层树通常都是值的图层树模型。
在iOS中,屏幕每秒钟重绘60次。如果动画时长比60分之一秒要长,Core Animation就需要在设置一次新值和新值生效之间,对屏幕上的图层进行重新组织。这意味着CALayer除了“真实”值(就是你设置的值)之外,必须要知道当前显示在屏幕上的属性值的记录。
每个图层属性的显示值都被存储在一个叫做呈现图层的独立图层当中,他可以通过-presentationLayer方法来访问。这个呈现图层实际上是模型图层的复制,但是它的属性值代表了在任何指定时刻当前外观效果。换句话说,你可以通过呈现图层的值来获取当前屏幕上真正显示出来的值。
呈现树通过图层树中所有图层的呈现图层所形成。注意呈现图层仅仅当图层首次被提交(就是首次第一次在屏幕上显示)的时候创建,所以在那之前调用-presentationLayer将会返回nil。
你可能注意到有一个叫做–modelLayer的方法。在呈现图层上调用–modelLayer将会返回它正在呈现所依赖的CALayer。通常在一个图层上调用-modelLayer会返回–self(实际上我们已经创建的原始图层就是一种数据模型)。
十、Tile-Based渲染
Tiled-Based 渲染是移动设备的主流。整个屏幕会分解成N*Npixels组成的瓦片(Tiles),tiles存储于SoC 缓存(SoC=system on chip,片上系统,是在整块芯片上实现一个复杂系统功能,如intel cpu,整合了集显,内存控制器,cpu运核心,缓存,队列、非核心和I/O控制器)。
几何形状会分解成若干个tiles,对于每一块tile,把必须的几何体提交到OpenGL ES,然后进行渲染(光栅化)。完毕后,将tile的数据发送回cpu。
GPU用来采集图片和形状,运行变换,应用文理和混合,最终把它们输送到屏幕上。
太多的几何结构会影响GPU速度,但这并不是GPU的瓶颈限制原因,但由于图层在显示之前要通过IPC发送到渲染服务器的时候(图层实际上是由很多小物体组成的特别重量级的对象),太多的图层就会引起CPU的瓶颈。
1、当前屏幕渲染的Tile-Based渲染流程
1、GPU 收到OpenGL ES处理完毕的 Command Buffer,包含图元 primitives 信息
2、Tiler 开始工作:先通过顶点着色器 Vertex Shader 对顶点进行处理,更新图元信息
3、平铺过程:平铺生成 tile bucket 的几何图形,这一步会将图元信息转化为像素,之后将结果写入 Parameter Buffer 中
4、Tiler 更新完所有的图元信息,或者 Parameter Buffer 已满,则会开始下一步4、Renderer,调用片元着色器,进行像素渲染;
5、Renderer 工作:调用片元着色器,进行像素渲染,将像素信息进行处理得到 bitmap,之后存入 Render Buffer
6、Render Buffer 中存储有渲染好的 bitmap,供之后的 Display 操作使用
2、离屏渲染(GPU) —— 遮罩(Mask)
1、渲染layer的mask纹理,同Tile-Based的基本渲染逻辑;
2、渲染layer的content纹理,同Tile-Based的基本渲染逻辑;
3、Compositing操作,合并1、2的纹理;
一般情况下,OpenGL会将应用提交到Render Server的动画直接渲染显示(基本的Tile-Based渲染流程),但对于一些复杂的图像动画的渲染并不能直接渲染叠加显示,而是需要根据Command Buffer分通道进行渲染之后再组合,这一组合过程中,就有些渲染通道是不会直接显示的;对比基本渲染通道流程和Masking渲染通道流程图,我们可以看到到Masking渲染需要更多渲染通道和合并的步骤;而这些没有直接显示在屏幕的上的通道(如上图的 Pass 1 和 Pass 2)就是Offscreen Rendering Pass。
Offscreen Render为什么卡顿,从上图我们就可以知道,Offscreen Render需要更多的渲染通道,而且不同的渲染通道间切换需要耗费一定的时间,这个时间内GPU会闲置,当通道达到一定数量,对性能也会有较大的影响;
3、离屏渲染(GPU)——UIVisiualEffectView毛玻璃高斯模糊
十一、UIImageView渲染
当显示一个UIImageView时,Core Animation会创建一个OpenGL ES纹理,并确保在这个图层中的位图被上传到对应的纹理中。当你重写-drawInContext方法时,Core Animation会请求分配一个纹理,同时确保Core Graphics会将你在-drawInContext中绘制的东西放入到纹理的位图数据中。
UIImage持有的数据是未解码的压缩数据,能节省较多的内存和加快存储。
当UIImage被赋值给UIImage时(例如imageView.image = image; Prepare阶段),图像数据会被解码为位图数据,变成RGB的颜色数据。
解码是一个计算量较大的任务,且需要CPU来执行;并且解码出来的图片体积与图片的宽高有关系,而与图片原来的体积无关。
其体积大小可简单描述为:宽 * 高 * 每个像素点的大小 = width * height * 4bytes。
如果要显示的只是缩略图,可以在UIImage从APP本地读取,或者从网络下载图片后,转换为要显示的大小并进行解码,这样再次赋值给UIImageView,此时的UIImage只是一个解码后的CGImage的封装,所以当UIImage赋值给UIImageView时,CALayer可以直接使用CGImage所持有的图像数据。
加载一个图片的流程大致为:
UIImage的图像被分配给UIImageView。
如果图像数据为未解码的PNG/JPG,解码为位图数据
隐式CATransaction捕获到UIImageView layer树的变化
在主运行循环的下一次迭代中,Core Animation提交隐式事务
GPU处理位图数据,进行渲染
+imageNamed:方法
使用[UIImage imageNamed:]加载图片有个好处在于可以立刻解压图片而不用等到绘制的时候。但是[UIImage imageNamed:]方法有另一个非常显著的好处:它在内存中自动缓存了解压后的图片,即使你自己没有保留对它的任何引用。
十二、性能优化
混合
GPU会放弃绘制那些完全被其他图层遮挡的像素,但是要计算出一个图层是否被遮挡也是相当复杂并且会消耗处理器资源。同样,合并不同图层的透明重叠像素(即混合)消耗的资源也是相当客观的。所以为了加速处理进程,不到必须时刻不要使用透明图层。任何情况下,你应该这样做:
- 给视图的backgroundColor属性设置一个固定的,不透明的颜色
- 设置opaque属性为YES
这样做减少了混合行为(因为编译器知道在图层之后的东西都不会对最终的像素颜色产生影响)并且计算得到了加速,避免了过度绘制行为因为Core Animation可以舍弃所有被完全遮盖住的图层,而不用每个像素都去计算一遍。
减少图层数量
初始化图层,处理图层,打包通过IPC发给渲染引擎,转化成OpenGL几何图形,这些是一个图层的大致资源开销。事实上,一次性能够在屏幕上显示的最大图层数量也是有限的。
如果你正在使用多个UILabel或者UIImageView实例等去显示固定内容,你可以把他们全部替换成一个单独的视图,然后用-drawRect:方法绘制出那些复杂的视图层级。这个提议看上去并不合理因为大家都知道软件绘制行为要比GPU合成要慢而且还需要更多的内存空间,但是在因为图层数量而使得性能受限的情况下,软件绘制很可能提高性能呢,因为它避免了图层分配和操作问题。
用Core Graphics去绘制一个静态布局有时候会比用层级的UIView实例来得快。
使用CALayer的-renderInContext:方法,你可以将图层及其子图层快照进一个Core Graphics上下文然后得到一个图片,它可以直接显示在UIImageView中,或者作为另一个图层的contents。不同于shouldRasterize —— 要求图层与图层树相关联 —— ,这个方法没有持续的性能消耗。
当图层内容改变时,刷新这张图片的机会取决于你(不同于shouldRasterize,它自动地处理缓存和缓存验证),但是一旦图片被生成,相比于让Core Animation处理一个复杂的图层树,你节省了相当客观的性能。
十三、GPU 渲染
CPU 虽然也是硬件,但是工作内容大部分是面向软件层面,需要处理很多中断指令。相对而言,GPU 工作内容是纯计算,其设计上也是为了高并发和高效率的计算,所以 GPU 渲染一般称为硬件加速。
渲染管线(Render Pipeline)
在渲染流程中,CPU与GPU正如上文一样通力合作渲染图像。在运算过程中,CPU如同进货的卡车不断地将要处理的数据丢给GPU,GPU工厂调动一个个如工人一般的计算单元对这些数据进行简单的处理,最后组装出产品——图像。
Application
即应用处理阶段(这里的应用不是指app,而是指软件层面),或者叫cpu处理阶段。
在接收到用户相应的输入(或者程序设定好的一系列操作),到将相应的图元(primitives)输入到GPU进行Rendering的这个阶段,就是Application Stage。
Application阶段的职责,可以简单理解为:定义当下要构建世界的模型图元,对GPU中的各个Shader进行编程和参数设定,最后将图元和这些信息(譬如渲染目标场景中的灯光、场景的模型、摄像机的位置)传递给GPU,GPU便可以帮助我们显示对应的每一帧。Application阶段结束后,会将渲染图元(rendering primitives)发往GPU做进一步的处理,这里的图元可以是点、线和三角形。
左边的图就犹如Application阶段的输出,站在一个上帝视角,决定这个世界的模型和我们的观察角度,右边就是经历完整条Rendering 管道后,GPU对应的输出结果,决定我们可以看到什么,即当前应该在显示器上显示什么。
Draw Call
在应用阶段,当CPU把数据传递到显存中后,CPU还不能一走了之,它还需要向GPU下达一个渲染指令,这个指令就是Draw Call。
Draw Call仅仅是一个指向需要被渲染的图元列表,没有其他材质信息。这整个过程就好比进货人员把一捆写好了原料信息、加工方法的原材料丢到工厂的仓库后对工人下达命令:“来!加工他!”。
Geometry Processing 几何处理阶段
Application结束后,GPU就拿到rendering primitives(渲染图元)和一些相关指令、参数,开始了在GPU中的旅程。首先,这些图元会经过Geometry Pipeline,在Real-time Rendering中,将Geometry Pipeline划分成了如下图的四个阶段。整个Geometry Processing阶段的目的是对输入的图元进行调整、修改、转换、增删等操作,在这整个阶段中,我们的处理对象是图元,输入的是图元,输出的还是图元。
这里之所以强调处理对象,是因为在Rendering Pipeline的后面,处理对象就从图元变成了像素(Pixel),即光栅化之后的操作。
流程图中展现了几何阶段中几个常见的渲染步骤(不同的图像应用接口存在着些许不同,这里以OpenGL为例),其中,绿色表示开发者可以完全编程控制的部分,虚线外框表示此阶段不是必需的,黄色表示开发者无法完全控制的部分(但可以进行一些配置),紫色表示开发者无法控制的阶段(已经由GPU固定实现)。
CPU向GPU发送的指令也是像流水线一样的——CPU往命令缓冲区中一个个放入命令,GPU则依次取出执行。在实际的渲染中,GPU的渲染速度往往超过了CPU提交命令的速度,这导致渲染中大部分时间都消耗在了CPU提交Draw Call上。有一种解决这种问题的方法是使用批处理(Batching),即把要渲染的模型合并在一起提交给GPU。打个比方,工厂想要把100根钢筋中间截断,如果发货方采用的方法是一根一根钢筋送给工厂,那速度肯定是相当慢的;大部分情况都是发货方把这100根钢筋打包送给工厂,这样明显加快了效率。
1. Vertex Shading
顶点着色器:因为每次处理顶点都是独立的,不需要额外考虑其他,所以进行这一步速度会相当快。
顶点着色器主要做以下事情:
1.Position视角转换。(必须)
将这些Vertex从模型空间转换到视角空间.
通过变换矩阵,以改变Vertex坐标系,得到各个Vertex的新Position。
2.Lighting, animation, texture coordinatesand others(可选)
通过CPU传入的光源信息,为每个Vertex添加相应的光照信息。
Vertex Shader可以改变Vertex的信息,可以使得Vertex移动到特定的位置,以改变物体形状,或者产生一些动作。就拿人物表情作为例子,假设角色的头部由一系列的Vertex组成,通过改变某些特定的Vertex,就可以构成人物各种生动丰富的表情。
上图中的人物主要是靠纹理(texture)来实现不同部位具有不同的颜色的效果,虽然在Vertex Shader阶段还不需要进行贴图操作,但是我们可以在这一阶段通过计算来得到每个Vertex对应的纹理坐标(texture coordinates),以方便后续的阶段使用该坐标。
逐顶点色彩处理
上面的光照和纹理等计算每个顶点信息的操作也可以叫做逐顶点色彩处理,值得一提的是,这里仅仅是“信息处理”,还不是真正的着色,可以理解为“为接下来的着色计算提供一些信息”。
2.Tessellation曲面细分着色器(可选)
Tessellation包括Hull Shader, Tesselator, Domain Shader三个阶段。整个Tessellation阶段的目的其实就是在已有图元的基础上,去加入更多的Vertex(这些Vertex还是在原始的图元内),以形成更精细的模型。
这些细分的顶点将有助于我们展现一个细节更加丰富的模型,这也是贴图置换(Displacement Mapping)的基本思路。
3.Geometry Shader几何着色器(可选)
与Tessellation在原有图元内部镶嵌三角形不同,Geometry Shader是在图元外添加额外的Vertex,将原始图元转换成新图元,以构建一个不一样的模型。
下图左边的三角形,通过添加三个Vertex,使得原先的三角形变成了六边形;通过对右边的线段添加额外的两个Vertex,以构成了一条折线。
有了Geometry Shader,我们就可以只用一个Vertex来表示每一个颗粒,只需要在Geometry Shader阶段将每一个Vertex拓展成两个三角形以形成一个正方形即可。
4.Projection投影
由于GPU最终是在屏幕上显示我们可以观看到的画面,所以Projection阶段需要决定我们的屏幕具体可以投影出什么内容。对于投影方式,有许多种,一般使用透视投影 (perspective projection)和正交投影/平行投影 (orthographic projection)。
虽然这里生成的是二维的平面,但是具体Z坐标信息都会存储在深度缓冲z-buffer中,后续的阶段还会使用到Z坐标的信息。
5.Clipping裁剪
将我们看不到的,也就是视野范围外的物体统统剔除掉。
我们屏幕最终可以显示的内容就是unit-cube中的内容,由于红色三角形在外部,对其进行剔除;橙色三角形有一部分在外部,对其添加两个新的Vertex,将外部的三角形剔除,最终转变成两个三角形;左下角的线条一部分在外面,直接在边缘交接处形成一个新的顶点,连接起来,再删除原有的外部顶点。这样,就完成了Clipping阶段的所有操作。
6.Screen Mapping屏幕映射
尽管GPU已经得到了顶点的x、y坐标,但他们处于[-1,1]区间中的,GPU还需要进行一定的计算才能把他们映射到我们的19201080甚至25601440的屏幕。
屏幕映射阶段,简而言之,就是将unit-cube中的坐标系转换到屏幕的坐标系,如下图所示,就是将Projection阶段后得到的x, y坐标系转换到(x1, y1)到(x2, y2)这个矩阵坐标系中。当然,对于z坐标系也需要进行转换,以映射到对应的坐标系中,默认是[0, 1],这个值可以通过API进行修改。
Rasterization光栅化
光栅化的目的是将Geometry Processing处理后的得到的图元(primitives)转换成一系列的像素(即picture elements),以方便后续的Pixel Processing,最终输出到屏幕上。Rasterization可以细分成Triangle Setup和Triangle Traversal,名字中含有Triangle并不意味着光栅化只处理三角形(当然还需要处理点和线图元),只是因为大部分的图元是三角形。
Rasterization这个过程是对所有的像素进行扫描遍历,然后决定哪些像素属于该图元。
Triangle Setup
这一个阶段也称作primitive assembly,也就是将一个个图元组装起来,会计算图元的边方程和一些其他信息,说白了就是将各个点连接起来,组成真正的三角形(或者连成线)。然后下一阶段会利用这些信息进行三角形的遍历,也就是检查有哪些像素位于该三角形图元内,对于点、线图元,就看他们覆盖了哪些像素。-
Triangle Traversal
为了将图元转换成像素,就需要识别每个图元覆盖了哪些像素,或者说,哪些像素处于图元的内部。那么,对于那些只有一部分在图元内部的像素,该怎么样去划分呢?一般情况下,我们会将下图中的绿色和黄色像素归为该图元的像素,为什么这样去划分呢?我们可以看到,每个像素的中心有一个点,我们便是用这个中心点来进行划分的,若中心点在图元内部,那么这个中心点所对应的像素就属于该图元。
还有其它划分方式,比如只要像素被图元触碰了,都属于该图元;只有整个像素被覆盖的情况下才属于图元像素。
判别是否属于该图元的像素也有多种处理方式,不一定是采样中心点。
比如下图,如果像素有四个采样点进行识别,就可以得到右边的结果,由于一半在内部一半在外部,所以最终像素的颜色为红色和白色的混合,变成粉红色。
片元(Fragment)
被覆盖的区域将生成一个片元,片元不是真正意义上的像素,而是包含了很多种状态的集合(譬如屏幕坐标、深度、法线、纹理等),这些状态用于最终计算出每个像素的颜色。
抗锯齿(Anti-aliasing)
不管用什么划分片元的方法,三角形边缘部分总会显得很锐利。当然,程序员们也想出了各种各样的抗锯齿方法来解决这个问题,譬如多重采样抗锯齿(MultiSampling Anti-Aliasing,MSAA),这种抗锯齿方法对中心点不在三角形内的边缘处采用不同程度的浓度进行计算。(上面四个采样点方案)
除此以外,GPU还将对覆盖区域的每个像素的深度进行插值计算。因为对于屏幕上的一个像素来说,它可能有着多个三角形的重叠,所以这一步对于后面计算遮挡、半透明等效果有着重要的作用。
简单地说,这一步将告诉接下来的步骤,一个个三角形是怎么样覆盖每个像素的。
Pixel Processing
通过上述的Rasterization阶段,我们就拿到了各个图元对应的像素,最后这个阶段要做的事情就是给每个Pixel填充上正确的颜色,然后通过一系列处理计算,得到相应的图像信息,最终输出到显示器上。Pixel Processing可以分为Pixel Shading和Merging两个阶段,这两个阶段也被称作Fragment Shading和Color Blending。
- Pixel Shading像素着色器(Fragment Shader片元着色器)
这个阶段的目的是给输入每一个Pixel赋予正确的颜色,这些颜色的来源可以是纹理、图元的顶点信息和光照信息等。
首先对于纹理信息,前面的阶段我们已经得到了每个Vertex Shader的纹理坐标,那么我们就可以拿着这个这个坐标去纹理单元中裁剪出这个图元对应的纹理,然后再给图元中的每个像素赋值。我们需要将对应的纹理粘到像素上,这里我们并不知道每个像素的纹理坐标,只知道每个Vertex的纹理坐标,但是通过图元的三个顶点,可以得到该图元的纹理块,将该纹理块的颜色信息,对应地赋值到该图元中的像素,便实现了纹理的颜色赋值。
之前在Vertex Shading阶段提到过,可以给每个Vertex加上对应的光照、阴影信息,那么这些信息怎么作用到图元中的每个像素呢?在GPU中是通过插值(Interpolate)来实现的,具体就是用三角形三个顶点的值,来得到三角形中某个点的相应信息。比如我们想知道下图p点的信息,我们只需要知道x1, x2, x3三点的信息,通过相应的数学公式求得三点对p点的贡献,就可以得到相应的信息。
还有一种计算光照信息的方法,称作Per pixel lighting,顾名思义,就是对每个像素进行光照处理。具体实现方法是用光源向量、射线向量、法线向量来计算每个像素的光照信息,这样的实现效果必然比插值的效果更好,同时,运算量会大大增加。目前在纹理中还可以加入向量信息,这样就可以对应知道每个像素的法线向量,通过这个向量,就可以实现凹凸的贴图光照效果。
- Merging
到了Merging这一阶段,输入的每个Pixel都有相应的颜色信息,被存储在Color buffer,这一阶段就是要整合这些Pixel信息,以得到具体一帧的图像信息,这一阶段也被称作ROP。主要的工作有两个:对片元进行测试(Test)并进行合并(Merge)。
之前提到过,每个Vertex在经过Projection阶段后,对应的Z坐标信息并不会丢弃,而是存放在z-buffer中,在上一阶段做颜色插值的同时,其实也会将z-buffer中的信息对应地插值到每个Pixel,也就是意味着这一阶段的每个Pixel其实是带有Z坐标的深度信息。除了Z坐标信息,每个Pixel会带有Alpha信息,即透明度,alpha与RGB信息一同被存放color buffer中。
有了这些深度信息和alpha信息,我们就可以做一系列的测试,以确定哪些Pixel会被遮挡,不需要显示;哪些Pixel透明度不为1,需要做Blending操作,以决定最终需要输出的图像。这些测试包括Alpha Test, Stencil Test(模版测试), Depth Test,Scissor Test(裁剪测试),通过这些测试,才能最终显示在我们的屏幕上。
在经过上面的层层测试后,片元颜色就会被送到颜色缓冲区。GPU会使用双重缓冲(Double Buffering)的策略,即屏幕上显示前置缓冲(Front Buffer),而渲染好的颜色先被送入后置缓冲(Back Buffer),再替换前置缓冲,以此避免在屏幕上显示正在光栅化的图元。
Rendering Pipeline总结
下图中绿色方框表示该阶段完全可编程,其中有两个绿色方框是虚线,表示这两个步骤是可选项,然后黄色方框表示该阶段可以使用参数进行配置,但是不可编程,蓝色方框表示该阶段完全固定,不可编程和配置。