Core Animation
-
Core Animation 这个名字可能让你以为它只是用来做动画的,但实际上它是从一个叫做 Layer Kit 这么一个不怎么和动画有关的名字演变而来,所以做动画这只是Core Animation特性的冰山一角。
-
Core Animation 实际上是一个复合引擎它的职责就是尽可能快地组合屏幕上不同的可视内容,这个内容是被分解成独立的图层,存储在一个叫做 图层树的体系之中。于是这个树形成了 UIKit 以及在 iOS 应用程序当中你所能在屏幕上看见的一切的基础。
-
本文主要介绍的是 Core Animation 中关于图形渲染的部分内容。
CALayer
Core Animation 的本质就是将 CALayer 中的内容转化为位图从而供硬件操作,所以想探究 iOS 的渲染原理必须先了解CALayer。
官方介绍:
-
CALayer 通常用于为 视图 提供后备存储,但也可以在没有 视图 的情况下用于显示内容。
-
CALayer 的主要工作是管理您提供的视觉内容,但图层本身具有可以设置的视觉属性,例如背景色、边框和阴影。
-
除了管理可视内容外,CALayer 还维护有关其内容的几何信息(例如其位置、大小和转换)用于在屏幕上显示内容。
-
修改图层的属性可以在图层的内容或几何上启动动画。
-
层对象通过采用 CAMediaTiming 协议来封装层及其动画的持续时间和速度,CAMediaTiming 协议定义了层的计时信息。
CALayer 和 UIView 的关系
-
一句话总结它们的关系就是:UIView 负责处理用户交互,负责绘制内容的则是它持有的那个 CALayer。
-
UIView 是 iOS 开发的核心类,它负责几乎所有的界面展示和用户交互,属于 UIKit。CALayer 是 Core Animation 的核心类,Core Animation 是一个跨平台的绘制框架(iOS 和 macOS)。UIView 和 CALayer 的关系是一一对应的,由UIView的层级关系形成了一种与之平行的 CALayer 的层级关系。
-
UIView 封装了很多 CALayer 的基础属性和功能,对于开发者简单易用。一些 UIView 没有暴露出来的 CALayer 功能:
CALayer 坐标系
- CALayer 具有除了 frame 、bounds 之外区别于 UIView 的其他位置属性。UIView 使用的所谓 frame 、bounds 、center 等属性,其实都是从 CALayer 中返回的。
frame
-
UIView 的 frame 描述了其在父视图坐标系下的位置和大小。CALayer 的 frame 是一个计算型属性,它是从 bounds 、anchorPoint 和 position 的值中派生出来的。为此属性指定新值时,图层会更改其 position 和 bounds 属性以匹配您指定的 layer。
-
frame 计算公式:
frame.x = position.x - anchorPoint.x * bounds.size.widthframe.y = position.y - anchorPoint.y * bounds.size.height
bounds
-
bounds 表示了 layer 在自己的坐标系下的 origin 和 size。
-
修改 bounds 的 size 会同样改变 frame 的 size。
-
修改 bounds 的 origin 会改变自己的坐标系,使得所有 subLayer 的位置受到影响。
- UIScrollView 正是利用这个特性实现滚动效果的。
position
-
position 表示 layer 在其 superLayer 坐标系中的位置,它表示为 layer 的一个点,其与 anchorPoint 的位置总是相对应的。
-
修改 position 的值同样会改变 frame 的 origin。
-
anchorPoint 表示 layer 的锚点。anchorPoint 的坐标是基于 layer 的内部单元坐标空间,即取值范围为 (0-1, 0-1)。
-
任何基于图层的几何操作都发生在锚点所在点。
-
position 表示 anchorPoint 在 superLayer 中的位置,那么改变 anchorPoint 的值会发生什么呢?
- 修改 position 和 anchorPoint 都不会对彼此造成影响,受影响的只有 frame 的 origin。
寄宿图
-
contents 是为 CALayer 提供显示内容的对象,其类型是 Any,但通常它指向 CGImageRef。
-
实际上,contents 属性保存了由设备渲染流水线渲染好的位图 bitmap(通常也被称为 backing store),而当设备屏幕进行刷新时,会从CALayer中读取生成好的 bitmap,进而呈现到屏幕上。
- BitMap 算法的核心思想是用bit数组来记录 0-1 两种状态,然后再将具体数据映射到这个比特数组的具体位置,这个比特位设置成 0 表示数据不存在,设置成 1 表示数据存在。
-
通常我们使用 contents 展示图片,即常用的 CGImage。除了使用系统的 UIImage 方法外,还可以通过 Core Image 实现图片的解码(将图片资源转为 bitmap),主要使用的方法为 CGBitmapContextCreate
-
除了 CGImage 我们还可以通过 Core Graphics 来绘制图片,在 CALayer 的 draw(in:) 中使用指定的 CGContext 来绘制图片到 contents 中。
CALayerDelegate
-
我们在显示内容通常不是通过直接操作 layer 层来实现,虽然可以通过直接设置 layer的 contents 属性来实现,但是比较麻烦。 我们可以通过 CALayerDelegate 来控制 layer 的显示内容,同样可以设置 layer 的 contents 属性和绘制内容。
-
func display(_ layer: CALayer)
- 在这个方法中我们可以设置 layer 的 contents 属性。
-
func draw(_ layer: CALayer, in ctx: CGContext)
- 在这个方法中我们还可以通过 CGContext 来自定义绘制功能。
图层树
-
CALayer 和 UIView 一样都有自己的树状结构,它们都可以有自己的 subLayer 和 subView。
-
对于一个 UIView 构成的树状视图层级结构,会有一个完全与之对应的 CALayer 构成的树状图层层级结构。我们可以脱离 UIView 单独添加 CALayer,所以 CALayer 的图层树决定了渲染的内容,在只需要显示的情况下可以添加 CALayer 而不是 UIView 来减少不必要的性能损耗。
-
iOS 有三种图层树,分别是 model layer tree(模型树)、presentation tree(演示树)、render tree(渲染树)。
Model Layer Tree
- Model Layer Tree 会存储图层树中各个节点的信息,比如我们常用的 frame、backgroundColor等等。Model Layer Tree 存储的值是这些图层的目标值,这些数据是我们可以开应用程序中设置的,我们任何对于 CALayer 的修改都能反应到 Model Layer Tree 中。
Presentation Tree
- Presentation Tree 是一个中间层,它存储的是正在动画运动中 CALayer 实时的数据。我们无法在应用程序中直接控制这部分数据,它是在 Render Server 中生成的,但我们可以通过 CAAnimation 来实现动画控制以及使用 presentationLayer 来访问这个 tree 中的属性。
Render Tree
- Render Tree 中存储着直接要提交到 Render Tree 上进行显示的数据,对于 Core Animation 来说也是不公开的,我们无法访问。
渲染原理
- iOS 中应用并不负责渲染而是由专门的渲染进程负责,即 Render Server。
Core Animation Pipeline
-
Handle Event:由 App 处理事件,比如用户的页面滑动、点击等操作,此过程中可能会改变页面的布局或视图层次。
-
Commit Transaction:App 通过 CPU 处理显示内容的前置计算工作,比如图层创建、布局计算、图片解码等任务,之后将计算好的图层打包给 Render Server。
-
Decode:打包好的图层需要经过解码。完成解码后需要等待下一个 RunLoop 才会执行下一步的操作。
-
Draw Calls:Core Animation 调用下层的渲染框架 (OpenGL ES 或 Metal) 来调度 GPU 进行渲染。
-
Render:由 GPU 进行渲染工作
-
Display:由图形显示硬件将内容显示到屏幕上。同样需要等 Render 结束后的下一个 RunLoop 进行。
Commit Transaction
- Commit Transaction 主要进行的是 Layout、Display、Prepare、Commit 四个操作。
屏幕成像
-
渲染结束后就需要将像素信息 (pixelmap) 显示到屏幕上了。
-
一个简单的屏幕成像流程包括:
-
iOS 设备使用了 Vsync + 双缓冲机制的显示策略
双缓冲机制
- 电子设备的屏幕处理速度与 CPU、GPU 的处理速度不同,需要缓冲区来存储数据。而使用一个缓冲区会存在读取和写入的并发问题,所以需要增加一个缓冲区来分别处理读取和写入操作。
- 双缓冲机制下,GPU 会将处理好的数据写入备用缓冲区中,视频控制器读取预先已经处理好的数据。在渲染结束后只需要将两个缓冲区的指向进行交换就可以开始下一帧的渲染了。
Vsync
- 屏幕的刷新速度与 CPU + GPU 的渲染速度相同时,两者对于缓冲区的置换时机一致,是最完美的情况。但这种情况是很难保持的,CPU + GPU 的渲染是一个很耗时的过程,如果屏幕开始扫描后才完成渲染就会出现屏幕撕裂的现象。
- Vsync (垂直同步信号)相当于给缓冲区上了一把锁,GPU 会等待屏幕发出 Vsync 后才进行缓冲区交换和新一帧的渲染。
Vsync 和 RunLoop
-
iOS 的显示系统是由 Vsync 驱动的,Render Server 在接收到 Vsync 后会触发图层的渲染。
-
Core Animation在RunLoop中注册了一个Observer,监听了BeforeWaiting和Exit事件。这个Observer的优先级是2000000,低于常见的其他Observer。
-
当一个 Handle Event 触发时,RunLoop 被唤醒,App 会执行一些操作,比如创建和调整视图层级,为视图添加一个动画。这些操作最终都会被 CALayer 捕获,在 RunLoop 完成全部处理后 Core Animation 响应 Exit 事件,并通过 CATransaction 提交到 Render Tree。
-
显示硬件在发送 Vsync 后,Render Server 开始渲染工作(即上文中的 Draw Calls 和 Render 操作), 将渲染好的数据传入 Back Buffer 作为下一帧的图像。
屏幕卡顿
- 根据上面的内容我们知道完成一次屏幕内容的更新需要在两次 Vsync 之间完成 CPU + GPU 的渲染操作。如果在两个 Vsync 之间没有完成所有渲染操作,那一帧的数据就会被丢弃,而屏幕的内容也会保持不变。这就是我们常见的屏幕卡顿问题的本质。
性能优化
合理使用 CALayer
-
利用懒加载等方法推迟 CALayer 的加载时间,避免在同一时间大量创建 CALayer。
-
如果不需要响应事件,尽量使用 CALayer 代替 UIView。
-
避免频繁地修改 CALayer 的图形属性,最好提前计算好相关属性,在需要时一次性更改。
-
光栅化 (shouldRasterize)。通过设置这个属性可以将 CALayer 保存到内存中,当短时间内需要多次复用 CALayer 时可以使用这个功能。需要注意的是光栅化会触发离屏渲染,对性能有额外的损耗。
图片解码
-
UIImage 的 imageWithContentsOfFile:等方法只是将图片数据存到内存里并没有进行解码,在图片显示到屏幕上时才会触发隐式解码,这一部分工作是放在主线程的。
-
我们可以通过 Core Image 的方法将图片拷贝并解码来实现图片的异步解码功能。具体实现方法参考 。
-
大图的解码会占用较多的内存资源,同时 iOS 也限制了 GPU 能处理的最大像素范围。因此实际需要显示的图像尺寸可能并不是很大,可以使用系统方法来进行降采样。
图像绘制
-
我们可以通过 draw: 相关方法来实现简单的图像绘制,并且 Core Graphics 是线程安全的,我们可以将它放到子线程执行。
-
由于 GPU 是依赖图层树递归渲染的,更复杂的图层结构会对性能造成影响。对于一些位置固定的纯展示的图层,我们可以通过自定义绘制来减少一些层级(比如利用 Core Image 将两张图片合成为一张)。
-
设置 CALayer 的圆角等属性会触发离屏渲染,我们同样可以用自定义绘制对图片实现预处理,来避免不必要的离屏渲染。
参考文档
-
blog.ibireme.com/2015/11/12/… iOS 界面卡顿的原理以及一些卡顿优化的方法。
-
About Core Animation 苹果关于 Core Animation 的官方介绍文档。