深度剖析iOS渲染

iOS App 图形图像渲染的基本流程:

1.CPU:完成对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制。

2.GPU:GPU拿到CPU计算好的显示内容,完成纹理的渲染, 渲染完成后将渲染结果放入帧缓冲区。

简单来说,CPU就像科学家要进行繁琐的计算、GPU就像小学生进行简单的1+1计算,但是 GPU是通过并行执行大量的线程来完成计算任务的。GPU的并行计算能力是CPU的几倍甚至几十倍,可以处理大量的数据和计算任务,提高计算效率和速度。所以GPU主要应用在图像处理中,因此,大多数CALayer的属性都是用GPU来绘制。

3.帧缓冲区(Frame Buffer)(双缓冲机制,不再赘述)

正常情况下,在当前屏幕显示的内容,由 GPU 渲染完成后放到当前屏幕的帧缓存区,不需要额外的渲染空间。我们知道 iPhone 的屏幕刷新率是 60Hz,也就是刷新一帧的时间是 16.67 ms, 每隔这段时间视频控制器就会去读一次缓存区的内容来显示。

假如 GPU 遇到性能瓶颈,导致无法在一帧内更新渲染结果到帧缓存区,那么从缓存区读到的会是上一帧的内容,导致帧率降低界面卡顿。

4.视频控制器:用来读取Frame Buffer中的数据的,GPU开始会绘制后缓存里的画面,然后视频控制器读取完前缓存的画面, 就会去读取后缓存里的画面,然后GPU再去绘制前缓存里的画面。

以上简单介绍了下iOS图像渲染。

接下来我们进一步深入了解。。。。。

CPU和GPU是如何渲染的?

首先给出一张图:iOS的渲染流水线。

上述图像渲染流水线中,除了第一部分 Application 阶段,后续主要都由 GPU 负责,为了方便后文讲解,先将 GPU 的渲染流程图展示出来:

上图就是一个三角形被渲染的过程中,GPU 所负责的渲染流水线。可以看到简单的三角形绘制就需要大量的计算,如果再有更多更复杂的顶点、颜色、纹理信息(包括 3D 纹理),那么计算量是难以想象的。这也是为什么 GPU 更适合于渲染流程。

接下来介绍的是iOS的渲染流水线(渲染管道)

1.Application 应用处理阶段:得到图元

这个阶段具体指的就是图像在应用中被处理的阶段,此时还处于 CPU 负责的时期。在这个阶段应用可能会对图像进行一系列的操作或者改变,最终将新的图像信息传给下一阶段。这部分信息被叫做图元信息,通常是三角形、线段、顶点等。见下图海豚就很直观了。

2.Geometry 几何处理阶段:处理图元

进入这个阶段之后,以及之后的阶段,就都主要由 GPU 负责了。此时 GPU 可以拿到上一个阶段传递下来的图元信息,GPU 会对这部分图元进行处理,之后输出新的图元。这一系列阶段包括:

  • 顶点着色器(Vertex Shader):这个阶段中会将图元中的顶点信息进行视角转换、添加光照信息、增加纹理等操作。
  • 形状装配(Shape Assembly):图元中的三角形、线段、点分别对应三个 顶点、两个 顶点、一个 顶点。这个阶段会将 顶点 连接成相对应的形状。
  • 几何着色器(Geometry Shader):额外添加额外的顶点,将原始图元转换成新图元,以构建一个不一样的模型。简单来说就是基于通过三角形、线段和点构建更复杂的几何图形。

3.Rasterization 光栅化阶段:图元转换为像素

光栅化的主要目的是将几何渲染之后的图元信息,转换为一系列的像素,以便后续显示在屏幕上。

4.Pixel 像素处理阶段:处理像素,得到位图

我们得到了图元所对应的像素,此时,我们需要给这些像素填充颜色和效果。

经过光栅化阶段得到了图元所对应的像素,此时,我们需要给这些像素填充颜色和效果。所以最后这个阶段就是给像素填充正确的内容,最终显示在屏幕上。这些经过处理、蕴含大量信息的像素点集合,被称作位图(bitmap)。也就是说,Pixel 阶段最终输出的结果就是位图,过程具体包含:

  • 片段着色器(Fragment Shader):对屏幕像素点着色。
  • 测试与混合(Tests and Blending):检查图层深度和透明度,从而判断片段的前后位置,以及是否应该被舍弃,并进行图层混合。

总的来说:

iOS的渲染分为四个部分:

  • 应用处理阶段:相当于 CPU 执行的部分,负责处理逻辑并提交渲染指令。
  • 几何处理:负责几何变换和顶点处理,转化为图元。
  • 光栅化:将图元转化为片段,生成像素数据。
  • 像素处理:片段着色、纹理映射等,输出到帧缓冲区(Frame Buffer)。

位图:

位图(Bitmap) 是一种数据结构。一个位图是由 n*m 个像素组成,每个像素的颜色信息由 RGB 组合或者灰度值表示。见下图:

介绍完iOS渲染流水线后(其实这里介绍的就是App图像渲染的基本流程的CPU和GPU部分),我们回到图像渲染的基本流程,接下来我们要介绍有关垂直同步信号和双缓冲机制的内容。

如何将已经渲染好的图像放到屏幕上?

这里先简单介绍下屏幕成像的原理:

显示器的电子束会从屏幕的左上角开始逐行扫描,屏幕上的每个点的图像信息都从帧缓冲器中的位图进行读取,在屏幕上对应地显示。扫描的流程如下图所示:

这时候就会出现一个问题:我如何知道屏幕下一个位置的像素是否已经渲染好了并且已经存放在了帧缓存区里面(这就是我们说的屏幕撕裂的问题)?这里就涉及到了两个机制:

  • 垂直同步信号
  • 双缓冲机制

1.垂直同步信号

当电子束完成一帧的扫描,将要从头开始扫描时,就会发出一个垂直同步信号。只有当视频控制器接收到 Vsync 之后,才会将帧缓冲器中的位图更新为下一帧,这样就能保证每次显示的都是同一帧的画面,因而避免了屏幕撕裂。

简单来说就是:帧缓冲器接收到信号了,才更新下一帧。

2.双缓冲机制

所以双缓冲机制会增加一个新的备用缓冲器(back buffer)。渲染结果会预先保存在 back buffer 中,在接收到 Vsync 信号的时候,视频控制器会将 back buffer 中的内容置换到 frame buffer 中,此时就能保证置换操作几乎在一瞬间完成(实际上是交换了内存地址)。

简单来说就是:GPU开始会绘制后缓存里的画面,然后视频控制器读取完前缓存的画面, 就会去读取后缓存里的画面,然后GPU再去绘制前缓存里的画面。

通过双缓冲机制,我们可以在60FPS(16.67ms)前,置换新的一帧屏幕图像。这样就能及时的更新屏幕图像了,就能解决屏幕撕裂问题。

掉帧

但是,如果在接收到 Vsync 之时 CPU 和 GPU 还没有渲染好新的位图,视频控制器就不会去替换 frame buffer 中的位图。这时屏幕就会重新扫描呈现出上一帧一模一样的画面。相当于两个周期显示了同样的画面,这就是所谓掉帧的情况。

这时候我们可以用三缓冲机制来优化CPU和GPU的效率,也就是在CPU和GPU的空闲时间再渲染一个区域。

总的来说:

  • 屏幕卡顿的根本原因:CPU 和 GPU 渲染流水线耗时过长,导致掉帧。
  • Vsync 与双缓冲的意义:强制同步屏幕刷新,以掉帧为代价解决屏幕撕裂问题。
  • 三缓冲的意义:合理使用 CPU、GPU 渲染性能,减少掉帧次数。

上述我们介绍了iOS中将内容数据通过一系列渲染后放到屏幕的详细过程,接下来将会讲解iOS的渲染框架,iOS 提供了多个渲染框架,帮助开发者管理渲染流程中的各个步骤。

见下图:

GPU Driver:上述软件框架相互之间也有着依赖关系,不过所有框架最终都会通过 OpenGL 连接到 GPU Driver,GPU Driver 是直接和 GPU 交流的代码块,直接与 GPU 连接。

OpenGL:是一个提供了 2D 和 3D 图形渲染的 API,它能和 GPU 密切的配合,最高效地利用 GPU 的能力,实现硬件加速渲染。OpenGL的高效实现(利用了图形加速硬件)一般由显示设备厂商提供,而且非常依赖于该厂商提供的硬件。OpenGL 之上扩展出很多东西,如 Core Graphics 等最终都依赖于 OpenGL,有些情况下为了更高的效率,比如游戏程序,甚至会直接调用 OpenGL 的接口。

Core Graphics:Core Graphics 是一个强大的二维图像绘制引擎,是 iOS 的核心图形库,常用的比如 CGRect 就定义在这个框架下。

Core Animation在 iOS 上,几乎所有的东西都是通过 Core Animation 绘制出来,它的自由度更高,使用范围也更广。

Core Image:Core Image 是一个高性能的图像处理分析的框架,它拥有一系列现成的图像滤镜,能对已存在的图像进行高效的处理。

Metal:Metal 类似于 OpenGL ES,也是一套第三方标准,具体实现由苹果实现。Core Animation、Core Image、SceneKit、SpriteKit 等等渲染框架都是构建于 Metal 之上的。

我们可以看到,在iOS上,几乎所有的东西都是通过 Core Animation 绘制出来,接下来我们深入了解Core Animation。

Core Animation:

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

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

简单来说就是用户能看到的屏幕上的内容都由 CALayer 进行管理。

CALayer 是屏幕显示的基础(存储 bitmap)

CALayer的简单构成:

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

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

CALayer 与 UIView 的关系

  1. CALayer 是 UIView 的属性之一,负责渲染和动画,提供可视内容的呈现
  2. UIView 提供了对 CALayer 部分功能的封装,同时也另外负责了交互事件的处理。

可得出其他关系:

相同的层级结构:我们对 UIView 的层级结构非常熟悉,由于每个 UIView 都对应 CALayer 负责页面的绘制,所以 CALayer 也具有相应的层级结构。

部分效果的设置:因为 UIView 只对 CALayer 的部分功能进行了封装,而另一部分如圆角、阴影、边框等特效都需要通过调用 layer 属性来设置。

是否响应点击事件:CALayer 不负责点击事件,所以不响应点击事件,而 UIView 会响应。

不同继承关系:CALayer 继承自 NSObject,UIView 由于要负责交互事件,所以继承自 UIResponder。

为什么要将CALayer和UIView分离开来?

将 CALayer 独立出来这样设计的主要原因就是为了职责分离,拆分功能,方便代码的复用。通过 Core Animation 框架来负责可视内容的呈现,这样在 iOS 和 OS X 上都可以使用 Core Animation 进行渲染。与此同时,两个系统还可以根据交互规则的不同来进一步封装统一的控件。

Core Animation的渲染流程:

(区别于前面讲的iOS渲染流流水线,两者均为iOS渲染基础但是服务的层次和方面不一样)

整个流水线一共有下面几个步骤:

Handle Events:这个过程中会先处理点击事件,这个过程中有可能会需要改变页面的布局和界面层次。

Commit Transaction :此时 app 会通过 CPU 处理显示内容的前置计算,比如布局计算、图片解码等任务,接下来会进行详细的讲解。之后将计算好的图层进行打包发给 Render Server

Decode :打包好的图层被传输到 Render Server 之后,首先会进行解码。注意完成解码之后需要等待下一个 RunLoop 才会执行下一步 Draw Calls

Draw Calls:解码完成后,Core Animation 会调用下层渲染框架(比如 OpenGL 或者 Metal)的方法进行绘制,进而调用到 GPU。

Render:这一阶段主要由 GPU 进行渲染。

Display :显示阶段,需要等 render 结束的下一个 RunLoop 触发显示。

深度了解Commit Transaction:

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

1.Layout:构建视图

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

  1. 调用重载的 layoutSubviews 方法
  2. 创建视图,并通过 addSubview 方法添加子视图
  3. 计算视图布局,即所有的 Layout Constraint

由于这个阶段是在 CPU 中进行,通常是 CPU 限制或者 IO 限制,所以我们应该尽量高效轻量地操作,减少这部分的时间,比如减少非必要的视图创建、简化布局计算、减少视图层级等。

2.Display:绘制视图

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

  1. 根据上一阶段 Layout 的结果创建得到图元信息。
  2. 如果重写了 drawRect: 方法,那么会调用重载的 drawRect: 方法,在 drawRect: 方法中手动绘制得到 bitmap 数据,从而自定义视图的绘制。

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

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

3.Prepare:Core Animation 额外的工作

这一步主要是:图片解码和转换

4.Commit:打包并发送

这一步主要是:当 runloop 在 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 状态时,会通知注册的监听,然后对图层打包,打完包后,将打包数据发送给一个独立负责渲染的进程 Render Server。图层打包并发送到 Render Server。

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

Render Server 的具体操作

Render Server 通常是 OpenGL 或者是 Metal。以 OpenGL 为例,那么上图主要是 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 操作使用

这里有张流程图总结的特别好:

以上就是iOS渲染的流程笔记。

离屏渲染

离屏渲染发生在iOS的渲染流程中(CPU、GPU、双缓冲器、视频控制器、屏幕)。我们知道,图像处理基本上是在 GPU中进行的,而GPU屏幕渲染有以下两种方式:

1.On-Screen Rendering:当前屏幕渲染,指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行。

正常情况下,如果要在显示屏上显示内容,我们至少需要一块与屏幕像素数据量一样大的帧缓冲区(frame buffer),作为像素数据存储区域,而这也是GPU存储渲染结果的地方。

2.Off-Screen Rendering:离屏渲染,指的是GPU在当前屏幕缓冲区以外新开辟一个(离屏)缓冲区进行渲染操作。

如果有时因为一些限制,无法把渲染结果直接写入frame buffer,而是先暂存在另外的内存区域,之后再写入frame buffer,那么这个过程被称之为离屏渲染。

其实

离屏渲染是有坏处的:

1.需要开辟一个新的缓存区,影响app性能。

2.因为是新的缓存区,此时有两个缓存区,那么GPU就要进行上下文切换来分别渲染,而上下文切换的代价是巨大的,会导致掉帧甚至卡顿。

那么,什么时候会触发离屏渲染呢?

我们再回顾下面layer的简单构成:

对于特定图层效果的使用,比如圆角,图层遮罩,阴影或者是图层光栅化 都会强制Core Animation提前渲染图层的离屏绘制(注意⚠️:一旦我们 为contents设置了内容 ,无论是图片、绘制内容、有图像信息的子视图等,再加上圆角+裁剪,就会触发离屏渲染,而当contens没有内容时,圆角和裁剪都不会触发离屏渲染 )。也就是说,当layer的contens中有东西(图片,色彩等)时, GPU为了将所有该绘画的绘画完了,再去为这个"结果"添加阴影或圆角,那么就需要离屏渲染存储layer。也就是说:阴影的本体(layer和其子layer)都还没有被组合到一起,怎么可能在第一步就画出只有完成最后一步之后才能知道的形状呢?这样一来又只能另外申请一块内存,把本体内容都先画好。

至于优化的话,有以下几种解决方法:

1.缓存已经处理好的结果,然后在复用的时候再拿出来用,得用shouldRasterize, 但是这个会导致至少一次离屏渲染,不过相对于有许多layer都各离屏渲染一次,这样处理就优化很多了。如果你的layer本来并不复杂,也没有圆角阴影等等,打开这个开关反而会增加一次不必要的离屏渲染

2.贝塞尔曲线优化

推荐大佬文章Texture | Corner Rounding

参考:[转] iOS离屏渲染原理及优化 · Tenloy's Blog

Core-Animation(三) - 渲染流程探究及性能分析 · Tenloy's Blog

iOS 离屏渲染探究 - 掘金

https://github.com/RickeyBoy/Rickey-iOS-Notes/blob/master/%E7%AC%94%E8%AE%B0/iOS%20Rendering.md

渲染(iOS渲染过程解析)_ios渲染流程-CSDN博客

相关推荐
恋猫de小郭14 小时前
什么?Flutter 可能会被 SwiftUI/ArkUI 化?全新的 Flutter Roadmap
flutter·ios·swiftui
网安墨雨18 小时前
iOS应用网络安全之HTTPS
web安全·ios·https
福大大架构师每日一题19 小时前
37.1 prometheus管理接口源码讲解
ios·iphone·prometheus
BangRaJun2 天前
LNCollectionView-替换幂率流体
算法·ios·设计
刘小哈哈哈2 天前
iOS 多个输入框弹出键盘处理
macos·ios·cocoa
靴子学长2 天前
iOS + watchOS Tourism App(含源码可简单复现)
mysql·ios·swiftui
一如初夏丿2 天前
xcode15 报错 does not contain ‘libarclite‘
ios·xcode
杨武博2 天前
ios 混合开发应用白屏问题
ios
BangRaJun3 天前
LNCollectionView
android·ios·objective-c
二流小码农3 天前
鸿蒙元服务项目实战:终结篇之备忘录搜索功能实现
android·ios·harmonyos