[TOC]
前言
- 最近在 iOS 开发中做了较多动画相关的功课,在进一步探索动画及渲染相关原理的时候,随着逐步深入了解发现这里面涉及到从硬件底层到软件框架等一系列相关知识。
- 为了了解
iOS系统的图像渲染原理
,我们首先对计算机图形渲染原理进行了一定了解并输出了01-计算机原理|计算机图形渲染原理、02-计算机原理|移动终端屏幕成像与卡顿这两篇文章; - 我们在01-计算机原理|计算机图形渲染原理这篇文章可以了解到
终端设备图形渲染的流水
线以及屏幕图像显示原理
; - 我们在02-计算机原理|移动终端屏幕成像与卡顿可以了解到终端设备
屏幕成像与卡顿
以及成像过程中会出现的问题和他们的解决方案
; - 紧接着,我们将沿着思路,进入了解iOS图形渲染框架,进一步深入去了解iOS系统可视化界面的渲染原理这个专题,为后续对相关专题的研究做知识储备。
一、iOS的各个渲染框架
在介绍渲染框架之前,我们得先了解一下iOS系统的渲染流水和具体的渲染技术栈
1.渲染技术栈
iOS 的渲染框架依然符合渲染流水线的基本架构,具体的技术栈如上图所示
- 在硬件基础之上,iOS 中有
Core Graphics
、Core Animation
、Core Image
、OpenGL
等多种软件框架来绘制内容,在 CPU 与 GPU 之间进行了更高层地封装
2.渲染技术栈的概念说明
①-应用交互前端UIKit/AppKit → ②-Core Animation → ③ OpenGL ES/ Metal → ④ GPU Driver →⑤ GPU → ⑥ Screen Display
-
UIKit/AppKit 是OC based API,其显示的内容基于CoreAnimation 这个符合渲染库的基础上建设的;
-
其点击等交互响应是依赖于"页面图层树上的UIResponder响应者链的基础上建设的;
-
第一层
- UIKit/AppKit
- UIKit 是 iOS 开发者最常用的框架,可以通过设置 UIKit 组件的布局以及相关属性来绘制界面。
- 事实上, UIKit 自身并不具备在屏幕成像的能力,其主要负责对用户操作事件的响应(UIView 继承自 UIResponder),事件响应的传递大体是经过逐层的 视图树 遍历实现的
- UIKit/AppKit
-
第二层
- Core Animation:
- Core Animation 源自于 Layer Kit,动画只是 Core Animation 特性的冰山一角。
- Core Animation 是一个复合引擎,其职责是 尽可能快地组合屏幕上不同的可视内容,这些可视内容可被分解成独立的图层(即 CALayer),这些图层会被存储在一个叫做图层树的体系之中。
- 从本质上而言,CALayer 是用户所能在屏幕上看见的一切的基础,几乎所有的东西都是通过 Core Animation 绘制出来,它的自由度更高,使用范围也更广。
- Core Graphics:
- Core Graphics 是一个基于 Quartz 的2D图像 高级绘图引擎,是 iOS 的核心图形库,主要用于运行时绘制图像。开发者可以使用此框架来处理基于路径的绘图、转换、颜色管理、离屏渲染、图案、渐变和阴影、图像数据管理、图像创建和图像遮罩以及 PDF 文档创建、显示和分析。
- 当开发者需要在 运行时创建图像 时,可以使用 Core Graphics 去绘制。与之相对的是 运行前创建图像,例如用 Photoshop 提前做好图片素材直接导入应用。相比之下,我们更需要 Core Graphics 去在运行时实时计算、绘制一系列图像帧来实现动画
- Core Image:
- Core Image 是一个高性能的图像处理分析的框架,它拥有一系列现成的图像滤镜,能对已存在的图像进行高效的处理。
- Core Image 与 Core Graphics 恰恰相反,Core Graphics 用于在 运行时创建图像,而 Core Image 是用来处理 运行前创建的图像 的。
- 大部分情况下,Core Image 会在 GPU 中完成工作,但如果 GPU 忙,会使用 CPU 进行处理
- Core Animation:
-
第三层
- OpenGL ES:
- OpenGL是一个提供了 2D 和 3D 图形渲染的 API,它能和 GPU 密切的配合,最高效地利用 GPU 的能力,实现硬件加速渲染。
- OpenGL的高效实现(利用了图形加速硬件)一般由显示设备厂商提供,而且非常依赖于该厂商提供的硬件。
- OpenGL 之上扩展出很多东西,如 Core Graphics 等最终都依赖于 OpenGL,有些情况下为了更高的效率,比如游戏程序,甚至会直接调用 OpenGL 的接口。
- OpenGL ES(OpenGL for Embedded Systems,简称 GLES),是 OpenGL 的子集。在移动设备中,采用的都是OpenGL的删减版PenGLES
- Metal:
- Metal 类似于 OpenGL ES,也是一套第三方标准,具体实现由苹果实现。大多数开发者都没有直接使用过 Metal,但其实所有开发者都在间接地使用 Metal。Core Animation、Core Image、SceneKit、SpriteKit 等等渲染框架都是构建于 Metal 之上的。
- 当在真机上调试 OpenGL 程序时,控制台会打印出启用 Metal 的日志。根据这一点可以猜测,Apple 已经实现了一套机制将 OpenGL 命令无缝桥接到 Metal 上,由 Metal 担任真正于硬件交互的工作。
- OpenGL ES:
-
第四层
- GPU Driver:
- 上述软件框架相互之间也有着依赖关系,不过所有框架最终都会通过 OpenGL 连接到 GPU Driver,GPU Driver 是直接和 GPU 交流的代码块,直接与 GPU 连接。
- GPU Driver:
二、iOS系统的复合引擎Core Animation
Render, compose, and animate visual elements. ---- Apple
1.Core Animation 简介
- Core Animation,它
本质上可以理解为一个复合引擎
,主要职责包含:渲染、构建和实现动画 - 通常我们会使用 Core Animation 来高效、方便地实现动画,但是实际上
它的前身叫做Layer Kit
,关于动画实现只是它功能中的一部分
。 - 对于 iOS app,不论是否直接使用了 Core Animation,它都
在底层深度参与了 app 的构建
。 - 而对于 OS X app,也可以通过使用 Core Animation 方便地实现部分功能。
- Core Animation 是 AppKit 和 UIKit 完美的底层支持,同时也被整合进入 Cocoa 和 Cocoa Touch 的工作流之中,它是
app 界面渲染和构建的最基础架构
。 - Core Animation 的职责就是尽可能快地组合屏幕上不同的可视内容,这个内容是被分解成独立的 layer(iOS 中具体而言就是 CALayer),并且被存储为树状层级结构。
- 这个树也形成了 UIKit 以及在 iOS 应用程序当中我们所能在屏幕上看见的一切的基础。
- 简而言之就是用户能看到的屏幕上的内容都由 CALayer 进行管理。那么 CALayer 究竟是如何进行管理的呢?
- 另外在 iOS 开发过程中,最大量使用的视图控件实际上是 UIView 而不是 CALayer,那么他们两者的关系到底如何呢?
- 我们将在后面的篇幅一一揭开这几个问题的面纱。
2.CALayer 是显示的基础:存储 bitmap「由bitmap可以联系到渲染过程去」
简单理解,CALayer 就是屏幕显示的基础。那 CALayer 是如何完成的呢?让我们来从源码向下探索一下,在 CALayer.h 中,CALayer 有这样一个属性 contents
-
An object providing the contents of the layer, typically a CGImageRef.
- contents 提供了 layer 的内容,是一个指针类型,在 iOS 中的类型就是 CGImageRef(在 OS X 中还可以是 NSImage)。
-
而我们进一步查到,Apple 对 CGImageRef 的定义是:A bitmap image or image mask.
- 看到 bitmap,这下我们就可以和之前讲的的渲染流水线联系起来了:
- 实际上,CALayer 中的 contents 属性保存了由设备渲染流水线渲染好的位图 bitmap(通常也被称为 backing store),而当设备屏幕进行刷新时,会从 CALayer 中读取生成好的 bitmap,进而呈现到屏幕上。
-
所以,如果我们在代码中对 CALayer 的 contents 属性进行了设置,比如这样:
-
那么在运行时,操作系统会调用底层的接口,将 image 通过 CPU+GPU 的渲染流水线渲染得到对应的 bitmap,存储于 CALayer.contents 中,在设备屏幕进行刷新的时候就会读取 bitmap 在屏幕上呈现。
-
也正因为每次要被渲染的内容是被静态的存储起来的,所以每次渲染时,Core Animation 会触发调用 drawRect: 方法,使用存储好的 bitmap 进行新一轮的展示
3.UIView 与 CALayer 的关系
UIView 作为最常用的视图控件,和 CALayer 也有着千丝万缕的联系,那么两者之间到底是个什么关系,他们有什么差异?
当然,两者有很多显性的区别,比如是否能够响应点击事件。但为了从根本上彻底搞懂这些问题,我们必须要先搞清楚两者的职责。
3.1 UIView的职责
根据 Apple 的官方文档,UIView 是 app 中的基本组成结构,定义了一些统一的规范。它会负责内容的渲染以及,处理交互事件。具体而言,它负责的事情可以归为下面三类:
- Drawing and animation:绘制与动画
- Layout and subview management:布局与子 view 的管理
- Event handling:点击事件处理
3.2 CALayer的职责
而从 CALayer 的官方文档中我们可以看出,CALayer 的主要职责是管理内部的可视内容,这也和我们前文所讲的内容吻合
当我们创建一个 UIView 的时候,UIView 会自动创建一个 CALayer,为自身提供存储 bitmap 的地方(也就是前文说的 backing store),并将自身固定设置为 CALayer 的代理
3.3 从这儿我们大概总结出下面两个核心关系:
- CALayer 是 UIView 的属性之一,负责渲染和动画,提供可视内容的呈现。
- UIView 提供了对 CALayer 部分功能的封装,同时也另外负责了交互事件的处理
- 为什么 UIKit 中的视图能够呈现可视化内容?就是因为 UIKit 中的每一个 UI 视图控件其实内部都有一个关联的 CALayer,即 backing layer
- CALayer 事实上是用户所能在屏幕上看见的一切的基础
3.4 有了这两个最关键的根本关系,那么下面这些经常出现在面试答案里的显性的异同就很好解释了。举几个例子:
- **相同的层级结构:**我们对 UIView 的层级结构非常熟悉,由于每个 UIView 都一一对应CALayer 负责页面的绘制,所以视图层级拥有 视图树 的树形结构,对应 CALayer 层级也拥有 图层树 的树形结构
- 其中,视图的职责是 创建并管理 图层,以确保当子视图在层级关系中 添加或被移除 时,其关联的图层在图层树中也有相同的操作,即保证视图树和图层树在结构上的一致性。
- **部分效果的设置:**因为 UIView 只对 CALayer 的部分功能进行了封装,而另一部分如圆角、阴影、边框等特效都需要通过调用 layer 属性来设置。
- **是否响应点击事件:**CALayer 不负责点击事件,所以不响应点击事件,而 UIView 会响应。
- **不同继承关系:**CALayer 继承自 NSObject,UIView 由于要负责交互事件,所以继承自 UIResponder。
3.5 当然还剩最后一个问题,那么为什么 iOS 要基于 UIView 和 CALayer 提供两个平行的层级关系呢?
为什么要将 CALayer 独立出来,直接使用 UIView 统一管理不行吗?为什么不用一个统一的对象来处理所有事情呢?
- 这样设计的主要原因就是为了职责分离,拆分功能,方便代码的复用;
- 通过 Core Animation 框架来负责可视内容的呈现,这样在 iOS 和 OS X 上都可以使用 Core Animation 进行渲染;
- 与此同时,两个系统还可以根据交互规则的不同来进一步封装统一的控件,比如 iOS 有 UIKit 和 UIView,OS X 则是AppKit 和 NSView。
- 实际上,这里并不是两个层级关系,而是四个。每一个都扮演着不同的角色。除了 视图树 和 图层树,还有 呈现树 和 渲染树。
4.CALayer显示可视化内容的原理
那么为什么 CALayer 可以呈现可视化内容呢?因为 CALayer 基本等同于一个 纹理。纹理是 GPU 进行图像渲染的重要依据。 在 计算机图形渲染原理 中提到纹理本质上就是一张图片,因此 CALayer 也包含一个 contents 属性指向一块缓存区,称为 backing store,可以存放位图(Bitmap)。iOS 中将该缓存区保存的图片称为 寄宿图 图形渲染流水线支持从顶点开始进行绘制(在流水线中,顶点会被处理生成纹理),也支持直接使用纹理(图片)进行渲染。相应地,在实际开发中,绘制界面也有两种方式:一种是 手动绘制;另一种是 使用图片。
对此,iOS 中也有两种相应的实现方式:
- 使用图片:contents image
- 手动绘制:custom drawing
4.1 Contents Image
- Contents Image 是指通过 CALayer 的 contents 属性来配置图片。然而,contents 属性的类型为 id。在这种情况下,可以给 contents 属性赋予任何值,app 仍可以编译通过。但是在实践中,如果 content 的值不是 CGImage ,得到的图层将是空白的。
- 既然如此,为什么要将 contents 的属性类型定义为 id 而非 CGImage。这是因为在 Mac OS 系统中,该属性对 CGImage 和 NSImage 类型的值都起作用,而在 iOS 系统中,该属性只对 CGImage 起作用。
- 本质上,contents 属性指向的一块缓存区域,称为 backing store,可以存放 bitmap 数据。
4.2 Custom Drawing
-
Custom Drawing 是指使用 Core Graphics 直接绘制寄宿图。实际开发中,一般通过继承 UIView 并实现 -drawRect: 方法来自定义绘制。
-
虽然 -drawRect: 是一个 UIView 方法,但事实上都是底层的 CALayer 完成了重绘工作并保存了产生的图片。
-
下图所示为 -drawRect: 绘制定义寄宿图的基本原理
-
UIView 有一个关联图层,即 CALayer。
-
CALayer 有一个可选的 delegate 属性,实现了 CALayerDelegate 协议。UIView 作为 CALayer 的代理实现了 CALayerDelegae 协议。
-
当需要重绘时,即调用 -drawRect:,CALayer 请求其代理给予一个寄宿图来显示。
-
CALayer 首先会尝试调用 -displayLayer: 方法,此时代理可以直接设置 contents 属性
- (void)displayLayer:(CALayer *)layer; 复制代码
-
如果代理没有实现 -displayLayer: 方法,CALayer 则会尝试调用 -drawLayer:inContext: 方法。在调用该方法前,CALayer 会创建一个空的寄宿图(尺寸由 bounds 和 contentScale 决定)和一个 Core Graphics 的绘制上下文,为绘制寄宿图做准备,作为 ctx 参数传入
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx; 复制代码
-
最后,由 Core Graphics 绘制生成的寄宿图会存入 backing store
5. Core Animation 渲染全内容
5.1 Core Animation Pipeline 渲染流水线
Core Animation 渲染流水线的工作原理
当我们了解了 Core Animation 以及 CALayer 的基本知识后通过前面的介绍,我们知道了 CALayer 的本质,那么它是如何调用 GPU 并显示可视化内容的呢?下面我们就需要介绍一下 Core Animation 渲染流水线的工作原理
- 事实上,app 本身并不负责渲染,渲染则是由一个独立的进程负责,即 Render Server 进程
- App 通过 IPC 将渲染任务及相关数据提交给 Render Server
- Render Server 处理完数据后,再传递至 GPU
- 最后由 GPU 调用 iOS 的图像设备进行显示
Core Animation 流水线的详细过程
- Handle Events: 首先,由 app 处理事件(Handle Events)
- 如:用户的点击操作,在此过程中 app 可能需要更新 视图树,相应地,图层树 也会被更新;
- Commit Transaction: 其次,app 通过 CPU 完成对显示内容的计算
- 如:视图的创建、布局计算、图片解码、文本绘制等。在完成对显示内容的计算之后,app 对图层进行打包,并在下一次 RunLoop 时将其发送至 Render Server,即完成了一次 Commit Transaction 操作;
- Render Server: Render Server 主要执行 Open GL/Metal、Core Graphics 相关程序,并调用 GPU;
- Decode: 打包好的图层被传输到 Render Server 之后,首先会进行解码。注意完成解码之后需要等待下一个 RunLoop 才会执行下一步 Draw Calls
- Draw Calls: 解码完成后,Core Animation 会调用下层渲染框架(比如 OpenGL 或者 Metal)的方法进行绘制,进而调用到 GPU
- Render: 这一阶段主要由 GPU 进行渲染,GPU 在物理层上完成了对图像的渲染
- Display: 显示阶段。最终,GPU 通过 Frame Buffer、视频控制器等相关部件,将图像显示在屏幕上。需要等 render 结束的下一个 RunLoop 才触发显示;
对上述步骤进行串联,它们执行所消耗的时间远远超过 16.67 ms,因此为了满足对屏幕的 60 FPS 刷新率的支持,需要将这些步骤进行分解,通过流水线的方式进行并行执行,如下图所示
5.2 Commit Transaction 发生了什么
一般开发当中能影响到的就是 Handle Events 和 Commit Transaction 这两个阶段,这也是开发者接触最多的部分。 Handle Events 就是处理触摸事件; 在 Core Animation 流水线中,app 调用 Render Server 前的最后一步 Commit Transaction 其实可以细分为 4 个步骤:
- Layout
- Display
- Prepare
- Commit
5.2.1 Layout(构建视图)
Layout 阶段主要进行视图构建和布局,具体步骤包括:
- 调用重载的 layoutSubviews 方法
- 创建视图,并通过 addSubview 方法添加子视图
- 计算视图布局,即所有的 Layout Constraint
由于这个阶段是在 CPU 中进行,通常是 CPU 限制或者 IO 限制,所以我们应该尽量高效轻量地操作,减少这部分的时间。比如减少非必要的视图创建、简化布局计算、减少视图层级等。
5.2.2 Display(绘制视图)
- 这个阶段主要是交给 Core Graphics 进行视图的绘制,注意不是真正的显示,而是得到前文所说的图元 primitives 数据:
- 根据上一阶段 Layout 的结果创建得到图元信息。
- 如果重写了 drawRect: 方法,那么会调用重载的 drawRect: 方法,在 drawRect: 方法中手动绘制得到 bitmap 数据,从而自定义视图的绘制
- 注意正常情况下 Display 阶段只会得到图元 primitives 信息,而位图 bitmap 是在 GPU 中根据图元信息绘制得到的
- 但是如果重写了 drawRect: 方法,这个方法会直接调用 Core Graphics 绘制方法得到 bitmap 数据,同时系统会额外申请一块内存,用于暂存绘制好的 bitmap;
- 由于
重写了 drawRect: 方法,导致绘制过程从 GPU 转移到了 CPU,这就导致了一定的效率损失
; - 与此同时,
这个过程会额外使用 CPU 和内存,因此需要高效绘制,否则容易造成 CPU 卡顿或者内存爆炸
;
5.2.3 Prepare(Core Animation 额外的工作)
Prepare 阶段属于附加步骤,一般处理图像的解码和转换等操作
5.2.4 Commit(打包并发送)
- 这一步主要是:将图层打包并发送到 Render Server
- 注意 commit 操作是依赖图层树递归执行的,所以如果图层树过于复杂,commit 的开销就会很大
- 这也是我们
希望减少视图层级,从而降低图层树复杂度的原因
5.3 Rendering Pass: Render Server 的具体操作
Render Server 通常是 OpenGL 或者是 Metal。以 OpenGL 为例,那么上图主要是 GPU 中执行的操作,具体主要包括:
- GPU 收到 Command Buffer,包含图元 primitives 信息
- Tiler 开始工作:先通过顶点着色器 Vertex Shader 对顶点进行处理,更新图元信息
- 平铺过程:平铺生成 tile bucket 的几何图形,这一步会将图元信息转化为像素,之后将结果写入 Parameter Buffer 中
- Tiler 更新完所有的图元信息,或者 Parameter Buffer 已满,则会开始下一步
- Renderer 工作:将像素信息进行处理得到 bitmap,之后存入 Render Buffer
- Render Buffer 中存储有渲染好的 bitmap,供之后的 Display 操作使用
相关阅读
[TOC]