01-iOS底层原理|iOS的各个渲染框架以及iOS图层渲染原理

4,943 阅读18分钟

[TOC]

通篇内容

前言

一、iOS的各个渲染框架

在介绍渲染框架之前,我们得先了解一下iOS系统的渲染流水和具体的渲染技术栈

1.渲染技术栈

iOS 的渲染框架依然符合渲染流水线的基本架构,具体的技术栈如上图所示

  • 在硬件基础之上,iOS 中有 Core GraphicsCore AnimationCore ImageOpenGL 等多种软件框架来绘制内容,在 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),事件响应的传递大体是经过逐层的 视图树 遍历实现的
  • 第二层
    • 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 进行处理
  • 第三层
    • 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 担任真正于硬件交互的工作。
  • 第四层
    • GPU Driver:
      • 上述软件框架相互之间也有着依赖关系,不过所有框架最终都会通过 OpenGL 连接到 GPU Driver,GPU Driver 是直接和 GPU 交流的代码块,直接与 GPU 连接。

二、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 阶段主要进行视图构建和布局,具体步骤包括:

  1. 调用重载的 layoutSubviews 方法
  2. 创建视图,并通过 addSubview 方法添加子视图
  3. 计算视图布局,即所有的 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 操作使用

相关阅读(共计14篇文章)

iOS相关专题

webApp相关专题

跨平台开发方案相关专题

阶段性总结:Native、WebApp、跨平台开发三种方案性能比较

Android、HarmonyOS页面渲染专题

小程序页面渲染专题

[TOC]