移动端渲染原理详解卷二(iOS渲染原理详解)

445 阅读9分钟

 iOS 中的渲染框架

移动端渲染原理详解卷一(基础知识)里面,已经详细的讲解了移动端(ios+android)渲染的基本原理,还没有看的小伙伴先去补课哦!
接下来,我将详细讲解iOS平台下渲染相关的原理,话不多说,先上图: 图像渲染流程图 iOS 的渲染框架依然符合渲染流水线的基本架构,具体的技术栈如上图所示。在硬件基础之上,iOS 中有 Core Graphics、Core Animation、Core Image、OpenGL 等多种软件框架来绘制内容,在 CPU 与 GPU 之间进行了更高层地封装。

APP层,我们能接触到的有:
UIKit:日常开发最常用的UI框架,可以通过设置UIKit组件的布局以及相关属性来绘制界面。其实本身UIView并不拥有屏幕成像的能力,而是View上的CALayer属性拥有展示能力。(UIView继承自UIResponder,其主要负责用户操作的事件响应,iOS事件响应传递就是经过视图树遍历实现的。)

SwiftUI:苹果在WWDC-2019推出的一款全新的“声明式UI”框架,使用Swift编写。一套代码,即可完成iOS、iPadOS、macOS、watchOS的开发与适配。

在APP层下面,有iOS最核心的与渲染系统库:

Core Animation:核心动画,一个复合引擎。尽可能快速的组合屏幕上不同的可视内容。分解成独立的图层(CALayer),存储在图层树中。

Core Graphics:基于 Quartz 高级绘图引擎,主要用于运行时绘制图像。比如 CGRect 就定义在这个框架下

Core Image:运行前图像绘制,对已存在的图像进行高效处理。

在核心渲染系统库的下面,便是iOS平台图形编程库,包含
OpenGL ES:OpenGL for Embedded Systems,简称 GLES,是 OpenGL 的子集。由GPU厂商定制实现,可通过C/C++编程操控GPU

Metal:由苹果公司开发用于取代OpenGL ES,能充分发挥苹果芯片优势。 现在,Core Animation、Core Image、SceneKit、SpriteKit 等渲染框架都是构建于 Metal 之上的。

GPU Driver:GPU Driver 是直接和 GPU 交流的代码块,可以直接驱动GPU。编程中,我们都通过OpenGL ES,metal等图形接口调用GPU Driver驱动GPU。

GPU -> Dispay:通过OpenGL ES,metal等图形接口,将渲染完成的数据,存储到Frame Buffer,在屏幕VSnyc信号到达之后,交换Frame Buffer完成界面显示。

从上面的介绍中,我们可以得出,iOS的渲染由多个系统库协同完成。接下来我们将讲解最重要也是功能最强的渲染框架Core Animation。

Core Animation详解

Render, compose, and animate visual elements. ---- Apple

Core Animation,它本质上可以理解为一个复合引擎,主要职责包含:渲染、构建和可见元素的动画。

通常,我们第一次了解 Core Animation 时,很容易将它理解为一个动画库。我们可以使用它方便地实现动画,但是实际上它的前身叫做 Layer Kit,关于动画实现只是它功能中的一部分,其他功能还包含视图的渲染界面的构建(视图的合成)等重要功能。所以对于 iOS app和OS X app,不管我们是否直接使用了 Core Animation,它都在底层深度参与了 app 的构建。

iOS渲染框架
Core Animation 是 AppKit 和 UIKit 完美的底层支持,同时也被整合进入 Cocoa 和 Cocoa Touch 的工作流之中,它是 app 界面渲染和构建的最基础架构。 Core Animation 的职责就是尽可能快地组合屏幕上不同的可视内容,这个内容是被分解成独立的 layer(iOS 中具体而言就是 CALayer),并且被存储为树状层级结构。这个树也形成了 UIKit 以及在 iOS 应用程序当中你所能在屏幕上看见的一切的基础。

简单来说就是用户能看到的屏幕上的内容都由 CALayer 进行管理。那么 CALayer 究竟是如何进行管理的呢?另外在 iOS 开发过程中,最大量使用的视图控件实际上是 UIView 而不是 CALayer,那么他们两者的关系到底如何呢?

CALayer 是显示的基础:存储 bitmap

简单理解,CALayer 是屏幕上的一个具有可见内容的矩形区域。那 CALayer 是使用什么存储和管理这块矩形区的域数据呢?

-   /** 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;

An object providing the contents of the layer, typically a CGImageRef.<br>

contents 提供了 layer 的内容,是一个指针类型,在 iOS 中的类型就是 CGImageRef(在 OS X 中还可以是 NSImage)。而我们进一步查到,Apple 对 CGImageRef 的定义是:<br>
A bitmap image or image mask.

看到 bitmap,这下我们就可以和之前讲的的渲染流水线联系起来了:实际上,CALayer 中的 contents 属性保存了由设备渲染流水线渲染好的位图 bitmap(通常也被称为 backing store),而当设备屏幕进行刷新时,会从 CALayer 中读取生成好的 bitmap,进而呈现到屏幕上。

所以,如果我们在代码中对 CALayer 的 contents 属性进行了设置,比如这样:

-   // 注意 CGImage 和 CGImageRef 的关系:
-   // typedef struct CGImage CGImageRef;
-   layer.contents = (__bridge id)image.CGImage;

那么在运行时,操作系统会调用底层的接口,将 image 通过 CPU+GPU 的渲染流水线渲染得到对应的 bitmap,存储于 CALayer.contents 中,在设备屏幕进行刷新的时候就会读取 bitmap 在屏幕上呈现。

也正因为每次要被渲染的内容是被静态的存储起来的,所以每次渲染时,Core Animation 会触发调用 drawRect: 方法,使用存储好的 bitmap 进行新一轮的展示,而不用每次都去计算和渲染需要显示的内容。

Core Animation Pipeline 渲染流水线

通过前面的介绍,我们知道了 CALayer 的本质,那么它是如何调用 GPU 并显示可视化内容的呢?下面我们就需要介绍一下 Core Animation 流水线的工作原理。

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

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

Core Animation 流水线的详细过程如下:

  • Handle Events,由 app 处理事件(Handle Events),如:用户的点击操作,在此过程中 app 可能需要更新 视图树,相应地,图层树 也会被更新。
  • Commit Transaction,app 通过 CPU 完成对显示内容的计算,如:视图的创建、布局计算、图片解码、文本绘制等。在完成对显示内容的计算之后,app 对图层进行打包,并在下一次 RunLoop 时将其发送至 Render Server,即完成了一次 Commit Transaction 操作。
  • Draw Calls 解码完成后,Core Animation 会调用下层渲染框架(比如Metal)的方法进行绘制,这里Draw Calls代表渲染任务由cpu提交给GPU的过程
  • Render GPU根据CommandBuffer完成可见视图的渲染和合成操作(也包含直接调用Metal库绘制的自定义内容,或者Metal的视频渲染)
  • Display,最终GPU将渲染完成的屏幕数据存储到Frame Buffer(后帧),最后视频控制器通过vsync驱动,Frame Buffer前后帧交换,显示屏硬件把内容显示在屏幕上。

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

Commit Transaction 发生了什么

一般开发当中能影响到的就是 Handle EventsCommit Transaction 这两个阶段,这也是开发者接触最多的部分。Handle Events 就是处理触摸事件,而 Commit Transaction 这部分中主要进行的是:Layout、Display、Prepare、Commit 等四个具体的操作。

Layout:构建视图

这个阶段主要处理视图的构建和布局,具体步骤包括:

调用重载的 layoutSubviews 方法
创建视图,并通过 addSubview 方法添加子视图
计算视图布局,即所有的 Layout Constraint
由于这个阶段是在 CPU 中进行,通常是 CPU 限制或者 IO 限制,所以我们应该尽量高效轻量地操作,减少这部分的时间,比如减少非必要的视图创建、简化布局计算、减少视图层级等。

Display:绘制视图

这个阶段主要是交给 Core Graphics 进行视图的绘制,注意不是真正的显示,而是得到前文所说的图元 primitives 数据:

根据上一阶段 Layout 的结果创建得到图元信息。
如果重写了 drawRect: 方法,那么会调用重载的 drawRect: 方法,在 drawRect: 方法中手动绘制得到 bitmap 数据,从而自定义视图的绘制。
注意正常情况下 Display 阶段只会得到图元 primitives 信息,而位图 bitmap 是在 GPU 中根据图元信息绘制得到的。但是如果重写了 drawRect: 方法,这个方法会直接调用 Core Graphics 绘制方法得到 bitmap 数据,同时系统会额外申请一块内存,用于暂存绘制好的 bitmap。

由于重写了 drawRect: 方法,导致绘制过程从 GPU 转移到了 CPU,这就导致了一定的效率损失。与此同时,这个过程会额外使用 CPU 和内存,因此需要高效绘制,否则容易造成 CPU 卡顿或者内存爆炸。

Prepare:Core Animation 额外的工作

额外的 Core Animation 工作,一般处理图像的解码 & 转换等操作

Commit:打包并发送

这一步主要是:图层打包并发送到 Render Server。

注意 commit 操作是依赖图层树递归执行的,所以如果图层树过于复杂,commit 的开销就会很大。这也是我们希望减少视图层级,从而降低图层树复杂度的原因。

Rendering Pass: Render Server 的具体操作

image.png

Render Server 通常是 OpenGL 或者是 Metal(现在基本是metal)。以 Metal 为例,那么上图主要是 GPU 中执行的操作,具体主要包括:

  1. GPU 收到 Command Buffer,包含图元 primitives 信息
  2. Tiler 开始工作:先通过顶点着色器 Vertex Shader 对顶点进行处理,更新图元信息
  3. 平铺过程:平铺生成 tile bucket 的几何图形,这一步会将图元信息转化为像素,之后将结果写入 Parameter Buffer 中
  4. Tiler 更新完所有的图元信息,或者 Parameter Buffer 已满,则会开始下一步
  5. Renderer 工作:将像素信息进行处理得到 bitmap,之后存入 Render Buffer
  6. Render Buffer 中存储有渲染好的 bitmap,供之后的 Display 操作使用

参考文献: