渲染(iOS渲染过程解析)

2023-12-14 23:33:59

渲染

渲染原理

一个硬核硬件科普视频

CPU和GPU

  • CPU(Central Processing Unit):现代计算机整个系统的运算核心、控制核心,适合串行计算。
  • GPU(Graphics Processing Unit):可进行绘图运算工作的专用微处理器,是连接计算机和显示终端的纽带,适合并行计算。

architecture

图像渲染流水线

图像渲染的计算量非常大,所以我们不能消耗大量CPU的资源去计算,转而使用拥有更强计算能力的GPU。

渲染流水线图如下

GraphicsPipeline

GPU渲染流程图如下

GPU图形渲染流水线

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

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

image-20231207013121937

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

这个阶段及之后就由GPU负责,这一阶段GPU拿到上一阶段图元信息,处理后输出新的图元,对应GPU渲染流程的以下操作:

  • 顶点着色器(Vertex Shader):3D坐标转2D坐标,确定顶点。
  • 形状装配(Shape Assembly):连接顶点成线。
  • 几何着色器(Geometry Shader):确定三角形的个数,使之变成几何图形。

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

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

rasterization

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

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

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

位图:

图片

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

屏幕成像与卡顿

在图像渲染流程结束之后,接下来就需要将得到的像素信息显示在物理屏幕上了。GPU 最后一步渲染结束之后像素信息,被存在帧缓冲器(Framebuffer)中,之后视频控制器(Video Controller)会读取帧缓冲器中的信息,经过数模转换传递给显示器(Monitor),进行显示。完整的流程如下图。

renderStructure

经过 GPU 处理之后的像素集合,也就是位图,会被帧缓冲器缓存起来,供之后的显示使用。显示器的电子束会从屏幕的左上角开始逐行扫描,屏幕上的每个点的图像信息都从帧缓冲器中的位图进行读取,在屏幕上对应地显示。扫描的流程如下图所示:

vsync

电子束扫描的过程中,屏幕就能呈现出对应的结果,每次整个屏幕被扫描完一次后,就相当于呈现了一帧完整的图像。屏幕不断地刷新,不停呈现新的帧,就能呈现出连续的影像。而这个屏幕刷新的频率,就是帧率(Frame per Second,FPS)。由于人眼的视觉暂留效应,当屏幕刷新频率足够高时(FPS 通常是 50 到 60 左右),就能让画面看起来是连续而流畅的。对于 iOS 而言,app 应该尽量保证 60 FPS 才是最好的体验。

屏幕撕裂 Screen Tearing

在这种单一缓存的模式下,最理想的情况就是一个流畅的流水线:每次电子束从头开始新的一帧的扫描时,CPU+GPU 对于该帧的渲染流程已经结束,渲染好的位图已经放入帧缓冲器中。但这种完美的情况是非常脆弱的,很容易产生屏幕撕裂:

tearing

CPU+GPU 的渲染流程是一个非常耗时的过程。如果在电子束开始扫描新的一帧时,位图还没有渲染好,而是在扫描到屏幕中间时才渲染完成,被放入帧缓冲器中 ---- 那么已扫描的部分就是上一帧的画面,而未扫描的部分则会显示新的一帧图像,这就造成屏幕撕裂。

垂直同步 Vsync + 双缓冲机制 Double Buffering

解决屏幕撕裂、提高显示效率的一个策略就是使用垂直同步信号 Vsync 与双缓冲机制 Double Buffering。根据苹果的官方文档描述,iOS 设备会始终使用 Vsync + Double Buffering 的策略。

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

但是这种情况下,视频控制器在接受到 Vsync 之后,就要将下一帧的位图传入,这意味着整个 CPU+GPU 的渲染流程都要在一瞬间完成,这是明显不现实的。所以双缓冲机制会增加一个新的备用缓冲器(back buffer)。渲染结果会预先保存在 back buffer 中,在接收到 Vsync 信号的时候,视频控制器会将 back buffer 中的内容置换到 frame buffer 中,此时就能保证置换操作几乎在一瞬间完成(实际上是交换了内存地址)。

gpu-double-buffer

掉帧 Jank

启用 Vsync 信号以及双缓冲机制之后,能够解决屏幕撕裂的问题,但是会引入新的问题:掉帧。如果在接收到 Vsync 之时 CPU 和 GPU 还没有渲染好新的位图,视频控制器就不会去替换 frame buffer 中的位图。这时屏幕就会重新扫描呈现出上一帧一模一样的画面。相当于两个周期显示了同样的画面,这就是所谓掉帧的情况。

double

如图所示,A、B 代表两个帧缓冲器,当 B 没有渲染完毕时就接收到了 Vsync 信号,所以屏幕只能再显示相同帧 A,这就发生了第一次的掉帧。

三缓冲 Triple Buffering

事实上上述策略还有优化空间。我们注意到在发生掉帧的时候,CPU 和 GPU 有一段时间处于闲置状态:当 A 的内容正在被扫描显示在屏幕上,而 B 的内容已经被渲染好,此时 CPU 和 GPU 就处于闲置状态。那么如果我们增加一个帧缓冲器,就可以利用这段时间进行下一步的渲染,并将渲染结果暂存于新增的帧缓冲器中。

tripple

如图所示,由于增加了新的帧缓冲器,可以一定程度上地利用掉帧的空档期,合理利用 CPU 和 GPU 性能,从而减少掉帧的次数。

屏幕卡顿的本质

手机使用卡顿的直接原因,就是掉帧。前文也说过,屏幕刷新频率必须要足够高才能流畅。对于 iPhone 手机来说,屏幕最大的刷新频率是 60 FPS,一般只要保证 50 FPS 就已经是较好的体验了。但是如果掉帧过多,导致刷新频率过低,就会造成不流畅的使用体验。

总结

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

iOS 中的渲染框架

softwareStack

iOS 的渲染框架依然符合渲染流水线的基本架构,具体的技术栈如上图所示。在硬件基础之上,iOS 中有 Core Graphics、Core Animation、Core Image、OpenGL 等多种软件框架来绘制内容,在 CPU 与 GPU 之间进行了更高层地封装。

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

OpenGL:是一个提供了 2D 和 3D 图形渲染的 API,它能和 GPU 密切的配合,最高效地利用 GPU 的能力,实现硬件加速渲染。

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

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

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

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

Core Animation

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

CA

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

CALayer 是显示的基础:存储 bitmap

简单理解,CALayer 就是屏幕显示的基础。CALayer 有一个属性 contents,它保存了由设备渲染流水线渲染好的位图 bitmap,而当设备屏幕进行刷新时,会从 CALayer 中读取生成好的 bitmap,进而呈现到屏幕上。

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

CALayer 与 UIView 的关系

即CoreAnimation和UIKit的关系:CoreAnimation 的核心类是 CALayer,UIKit 框架的核心类是 UIView

UIView职责:

  • Drawing and animation:绘制与动画
  • Layout and subview management:布局与子 view 的管理
  • Event handling:点击事件处理

当我们创建一个 UIView 的时候,UIView 会自动创建一个 CALayer,为自身提供存储 bitmap 的地方,并将自身固定设置为 CALayer 的代理。

image-20231207015453650

总结出下面两个核心关系

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

可得出其他关系:

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

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

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

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

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

Core Animation 渲染流程

Core Animation Pipeline 渲染流水线

image-20231207015925123

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

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 等四个具体的操作。

Layout:构建视图

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

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

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

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 卡顿或者内存爆炸。

Prepare:Core Animation 额外的工作

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

Commit:打包并发送

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

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

Rendering Pass: Render Server 的具体操作

rendering

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-原生渲染流程

离屏渲染

离屏渲染具体过程

通常的渲染流程是这样的,如下图。

offscreen1

App 通过 CPU 和 GPU 的合作,不停地将内容渲染完成放入 Framebuffer 帧缓冲器中,而显示屏幕不断地从 Framebuffer 中获取内容,显示实时的内容。

而离屏渲染的流程是这样的,如下图。

offscreen2

与普通情况下 GPU 直接将渲染好的内容放入 Framebuffer 中不同,需要先额外创建离屏渲染缓冲区 Offscreen Buffer,将提前渲染好的内容放入其中,等到合适的时机再将 Offscreen Buffer 中的内容进一步叠加、渲染,完成后将结果切换到 Framebuffer 中。

离屏渲染的效率问题

离屏渲染时由于 App 需要提前对部分内容进行额外的渲染并保存到 Offscreen Buffer,以及需要在必要时刻对 Offscreen Buffer 和 Framebuffer 进行内容切换,所以会需要更长的处理时间(实际上这两步关于 buffer 的切换代价都非常大)。

并且 Offscreen Buffer 本身就需要额外的空间,大量的离屏渲染可能早能内存的过大压力。与此同时,Offscreen Buffer 的总大小也有限,不能超过屏幕总像素的 2.5 倍。

可见离屏渲染的开销非常大,一旦需要离屏渲染的内容过多,很容易造成掉帧的问题。所以大部分情况下,我们都应该尽量避免离屏渲染。

为什么使用离屏渲染

  1. 一些特殊效果需要使用额外的 Offscreen Buffer 来保存渲染的中间状态,所以不得不使用离屏渲染。
  2. 处于效率目的,可以将内容提前渲染保存在 Offscreen Buffer 中,达到复用的目的。

对于第一种情况,也就是不得不使用离屏渲染的情况,一般都是系统自动触发的,比如阴影、圆角等等。

最常见的情形之一就是:使用了 mask 蒙版。

masking

如图所示,由于最终的内容是由两层渲染结果叠加,所以必须要利用额外的内存空间对中间的渲染结果进行保存,因此系统会默认触发离屏渲染。

又比如下面这个例子,iOS 8 开始提供的模糊特效 UIBlurEffectView:

UIVisualEffectView

整个模糊过程分为多步:Pass 1 先渲染需要模糊的内容本身,Pass 2 对内容进行缩放,Pass 3 4 分别对上一步内容进行横纵方向的模糊操作,最后一步用模糊后的结果叠加合成,最终实现完整的模糊特效。

而第二种情况,为了复用提高效率而使用离屏渲染一般是主动的行为,是通过 CALayer 的 shouldRasterize 光栅化操作实现的。

shouldRasterize 光栅化

开启光栅化后,会触发离屏渲染,Render Server 会强制将 CALayer 的渲染位图结果 bitmap 保存下来,这样下次再需要渲染时就可以直接复用,从而提高效率。

而保存的 bitmap 包含 layer 的 subLayer、圆角、阴影、组透明度 group opacity 等,所以如果 layer 的构成包含上述几种元素,结构复杂且需要反复利用,那么就可以考虑打开光栅化。

圆角、阴影、组透明度等会由系统自动触发离屏渲染,那么打开光栅化可以节约第二次及以后的渲染时间。而多层 subLayer 的情况由于不会自动触发离屏渲染,所以相比之下会多花费第一次离屏渲染的时间,但是可以节约后续的重复渲染的开销。

不过使用光栅化的时候需要注意以下几点:

  1. 如果 layer 不能被复用,则没有必要打开光栅化
  2. 如果 layer 不是静态,需要被频繁修改,比如处于动画之中,那么开启离屏渲染反而影响效率
  3. 离屏渲染缓存内容有时间限制,缓存内容 100ms 内如果没有被使用,那么就会被丢弃,无法进行复用
  4. 离屏渲染缓存空间有限,超过 2.5 倍屏幕像素大小的话也会失效,无法复用

圆角的离屏渲染

通常来讲,设置了 layer 的圆角效果之后,会自动触发离屏渲染。但是究竟什么情况下设置圆角才会触发离屏渲染呢?

layer_detail

如上图所示,layer 由三层组成,我们设置圆角通常会首先像下面这行代码一样进行设置:

复制代码view.layer.cornerRadius = 2

根据 cornerRadius - Apple 的描述,上述代码只会默认设置 backgroundColor 和 border 的圆角,而不会设置 content 的圆角,除非同时设置了 layer.masksToBounds 为 true(对应 UIView 的 clipsToBounds 属性):

如果只是设置了 cornerRadius 而没有设置 masksToBounds,由于不需要叠加裁剪,此时是并不会触发离屏渲染的。而当设置了裁剪属性的时候,由于 masksToBounds 会对 layer 以及所有 subLayer 的 content 都进行裁剪,所以不得不触发离屏渲染。

view.layer.masksToBounds = true // 触发离屏渲染的原因

所以,Texture 也提出在没有必要使用圆角裁剪的时候,尽量不去触发离屏渲染而影响效率:

corner-rounding-overlap

离屏渲染的具体逻辑

刚才说了圆角加上 masksToBounds 的时候,因为 masksToBounds 会对 layer 上的所有内容进行裁剪,从而诱发了离屏渲染,那么这个过程具体是怎么回事呢,下面我们来仔细讲一下。

图层的叠加绘制大概遵循“画家算法”,在这种算法下会按层绘制,首先绘制距离较远的场景然后用绘制距离较近的场景覆盖较远的部分。

painter

在普通的 layer 绘制中,上层的 sublayer 会覆盖下层的 sublayer,下层 sublayer 绘制完之后就可以抛弃了,从而节约空间提高效率。所有 sublayer 依次绘制完毕之后,整个绘制过程完成,就可以进行后续的呈现了。假设我们需要绘制一个三层的 sublayer,不设置裁剪和圆角,那么整个绘制过程就如下图所示:

normal

而当我们设置了 cornerRadius 以及 masksToBounds 进行圆角 + 裁剪时,如前文所述,masksToBounds 裁剪属性会应用到所有的 sublayer 上。这也就意味着所有的 sublayer 必须要重新被应用一次圆角+裁剪,这也就意味着所有的 sublayer 在第一次被绘制完之后,并不能立刻被丢弃,而必须要被保存在 Offscreen buffer 中等待下一轮圆角+裁剪,这也就诱发了离屏渲染,具体过程如下:

corner

实际上不只是圆角+裁剪,如果设置了透明度+组透明(layer.allowsGroupOpacity+layer.opacity),阴影属性(shadowOffset 等)都会产生类似的效果,因为组透明度、阴影都是和裁剪类似的,会作用与 layer 以及其所有 sublayer 上,这就导致必然会引起离屏渲染。

避免圆角离屏渲染

除了尽量减少圆角裁剪的使用,还有什么别的办法可以避免圆角+裁剪引起的离屏渲染吗?

由于刚才我们提到,圆角引起离屏渲染的本质是裁剪的叠加,导致 masksToBounds 对 layer 以及所有 sublayer 进行二次处理。那么我们只要避免使用 masksToBounds 进行二次处理,而是对所有的 sublayer 进行预处理,就可以只进行“画家算法”,用一次叠加就完成绘制。

那么可行的实现方法大概有下面几种:

  1. 【换资源】直接使用带圆角的图片,或者替换背景色为带圆角的纯色背景图,从而避免使用圆角裁剪。不过这种方法需要依赖具体情况,并不通用。
  2. 【mask】再增加一个和背景色相同的遮罩 mask 覆盖在最上层,盖住四个角,营造出圆角的形状。但这种方式难以解决背景色为图片或渐变色的情况。
  3. 【UIBezierPath】用贝塞尔曲线绘制闭合带圆角的矩形,在上下文中设置只有内部可见,再将不带圆角的 layer 渲染成图片,添加到贝塞尔矩形中。这种方法效率更高,但是 layer 的布局一旦改变,贝塞尔曲线都需要手动地重新绘制,所以需要对 frame、color 等进行手动地监听并重绘。
  4. 【CoreGraphics】重写 drawRect:,用 CoreGraphics 相关方法,在需要应用圆角时进行手动绘制。不过 CoreGraphics 效率也很有限,如果需要多次调用也会有效率问题。
  • 对于一些圆角可以创建四个背景颜色弧形的 layer 盖住四个角,从视觉上制造圆角的效果
  • 对于 view 的圆形边框,如果没有 backgroundColor,可以放心使用 cornerRadius 来做
  • 对于所有的阴影,使用 shadowPath 来规避离屏渲染
  • 对于特殊形状的 view,使用 layer mask 并打开 shouldRasterize 来对渲染结果进行缓存

触发离屏渲染原因的总结

总结一下,下面几种情况会触发离屏渲染:

  1. 使用了 mask 的 layer (layer.mask)
  2. 需要进行裁剪的 layer (layer.masksToBounds / view.clipsToBounds)
  3. 设置了组透明度为 YES,并且透明度不为 1 的 layer (layer.allowsGroupOpacity/layer.opacity)
  4. 添加了阴影的 layer (layer.shadow)
  5. 采用了光栅化的 layer (layer.shouldRasterize)
  6. 绘制了文字的 layer (UILabel, CATextLayer, Core Text 等)

不过,需要注意的是,重写 drawRect: 方法并不会触发离屏渲染。前文中我们提到过,重写 drawRect: 会将 GPU 中的渲染操作转移到 CPU 中完成,并且需要额外开辟内存空间。但根据苹果工程师的说法,这和标准意义上的离屏渲染并不一样,在 Instrument 中开启 Color offscreen rendered yellow 调试时也会发现这并不会被判断为离屏渲染。

图形处理相关框架

image-20231207024022053

1、iOS与图形图像处理相关的框架汇总:

  • 界面图形框架 – UIKit
  • 核心动画框架 – Core Animation
  • 苹果封装的图形框架 – Core Graphics & Quartz 2D
  • 传统跨平台图形框架 – OpenGL ES
  • 苹果最新力推的图形框架 – Metal
  • 适合图片的苹果滤镜框架 – Core Image
  • 适合视频的第三方滤镜方案 -- GPUImage (第三方不属于系统,这里列出来学习)
  • 游戏引擎 – Scene Kit (3D) 和 Sprite Kit (2D)
  • 计算机视觉在iOS的应用 – OpenCV for iOS

毫无疑问,开发者们接触得最多的框架是以下几个,UIKit、Core Animation,Core Graphic, Core Image。下面简要介绍这几个框架,顺便介绍下GPUImage:

2、界面图形框架 – UIKit(穿插使用其他图形处理框架)

  • UIKit是一组Objective-C API,为线条图形、Quartz图像和颜色操作提供Objective-C 封装,并提供2D绘制、图像处理及用户接口级别的动画。

  • UIKit包括UIBezierPath(绘制线、角度、椭圆及其它图形)、UIImage(显示图像)、UIColor(颜色操作)、UIFont和UIScreen(提供字体和屏幕信息)等类以及在位图图形环境、PDF图形环境上进行绘制和 操作的功能等, 也提供对标准视图的支持,也提供对打印功能的支持。

  • UIKit与Core Graphics的关系:

    在UIKit中,UIView类本身在绘制时自动创建一个图形环境,即Core Graphics层的CGContext类型,作为当前的图形绘制环境。在绘制时可以调用 UIGraphicsGetCurrentContext 函数获得当前的图形环境;

3、核心动画框架 – Core Animation

  • Core Animation 是常用的框架之一。它比 UIKit 和 AppKit 更底层。正如我们所知,UIView底下封装了一层CALayer树,Core Animation 层是真正的渲染层,我们之所以能在屏幕上看到内容,真正的渲染工作是在 Core Animation 层进行的。
  • Core Animation 是一套Objective-C API,实现了一个高性能的复合引擎,并提供一个简单易用的编程接口,给用户UI添加平滑运动和动态反馈能力。
  • Core Animation 是 UIKit 实现动画和变换的基础,也负责视图的复合功能。使用Core Animation可以实现定制动画和细粒度的动画控制,创建复杂的、支持动画和变换的layered 2D视图
  • OpenGL ES的内容也可以与Core Animation内容进行集成。
  • 为了使用Core Animation实现动画,可以修改 层的属性值 来触发一个action对象的执行,不同的action对象实现不同的动画。Core Animation 提供了一组基类及子类,提供对不同动画类型的支持:
    • CAAnimation 是一个抽象公共基类,CAAnimation采用CAMediaTiming 和CAAction协议为动画提供时间(如周期、速度、重复次数等)和action行为(启动、停止等)。
    • CAPropertyAnimation 是 CAAnimation的抽象子类,为动画提供一个由一个key路径规定的层属性的支持;
    • CABasicAnimation 是CAPropertyAnimation的具体子类,为一个层属性提供简单插入能力。
    • CAKeyframeAnimation 也是CAPropertyAnimation的具体子类,提供key帧动画支持。

4、苹果封装的图形框架 – Core Graphics & Quartz 2D

  • Core Graphics(使用Quartz 2D引擎)
    • Core Graphics是一套C-based API, 支持向量图形,线、形状、图案、路径、剃度、位图图像和pdf 内容的绘制
    • Core Graphics 也是常用的框架之一。它用于运行时绘制图像。开发者们可以通过 Core Graphics 绘制路径、颜色。当开发者需要在运行时创建图像时,可以使用 Core Graphics 去绘制,运行时实时计算、绘制一系列图像帧来实现动画。与之相对的是运行前创建图像(例如从磁盘中或内存中已经创建好的UIImage图像)。
  • Quartz 2D
    • Quartz 2D是Core Graphics中的2D 绘制呈现引擎。Quartz是资源和设备无关的,提供路径绘制,anti-aliased呈现,剃度填充图案,图像,透明绘制和透明层、遮蔽和阴影、颜色管理,坐标转换,字体、offscreen呈现、pdf文档创建、显示和分析等功能。
    • Quartz 2D能够与所有的图形和动画技术(如Core Animation, OpenGL ES, 和 UIKit 等)一起使用。Quartz 2D采用paint模式进行绘制。
    • Quartz 2D提供的主要类包括:
      • CGContext:表示一个图形环境;
      • CGPath:使用向量图形来创建路径,并能够填充和stroke;
      • CGImage:用来表示位图;
      • CGLayer:用来表示一个能够用于重复绘制和offscreen绘制的绘制层;
      • CGPattern:用来表示Pattern,用于重复绘制;
      • CGShading和 CGGradient:用于绘制剃度;
      • CGColor 和 CGColorSpace;用来进行颜色和颜色空间管理;
      • CGFont, 用于绘制文本;
      • CGPDFContentStream、CGPDFScanner、CGPDFPage、CGPDFObject,CGPDFStream, CGPDFString等用来进行pdf文件的创建、解析和显示。

5、适合图片的苹果滤镜框架 – Core Image

  • Core Image 与 Core Graphics 恰恰相反,Core Graphics 用于在运行时创建图像,而 Core Image 是用来处理已经创建的图像的。Core Image 框架拥有一系列现成的图像过滤器,能对已存在的图像进行高效的处理。
  • Core Image 是 iOS5 新加入到 iOS 平台的一个图像处理框架,提供了强大高效的图像处理功能, 用来对基于像素的图像进行操作与分析, 内置了很多强大的滤镜(Filter) (目前数量超过了180种), 这些Filter 提供了各种各样的效果, 并且还可以通过 滤镜链 将各种效果的 Filter叠加 起来形成强大的自定义效果。
    • 一个 滤镜 是一个对象,有很多输入和输出,并执行一些变换。例如,模糊滤镜可能需要输入图像和一个模糊半径来产生适当的模糊后的输出图像。
    • 一个 滤镜链 是一个链接在一起的滤镜网络,使得一个滤镜的输出可以是另一个滤镜的输入。以这种方式,可以实现精心制作的效果。
    • iOS8 之后更是支持自定义 CIFilter,可以定制满足业务需求的复杂效果。
  • Core Image 的优点在于十分高效。大部分情况下,它会在 GPU 中完成工作,但如果 GPU 忙,会使用 CPU 进行处理。如果设备支持 Metal,那么会使用 Metal 处理。这些操作会在底层完成,Apple 的工程师们已经帮助开发者们完成这些操作了。
  • Core Image 的 API 主要就是三类:
    • CIImage 保存图像数据的类,可以通过UIImage,图像文件或者像素数据来创建,包括未处理的像素数据。
    • CIFilter 表示应用的滤镜,这个框架中对图片属性进行细节处理的类。它对所有的像素进行操作,用一些键-值设置来决定具体操作的程度。
    • CIContext 表示上下文,如 Core Graphics 以及 Core Data 中的上下文用于处理绘制渲染以及处理托管对象一样,Core Image 的上下文也是实现对图像处理的具体对象。可以从其中取得图片的信息。

6、适合视频的第三方滤镜方案 – GPUImage

  • GPUImage

    是一个基于OpenGL ES 2.0的开源的图像处理库,优势:

    • 最低支持 iOS 4.0,iOS 5.0 之后就支持自定义滤镜。在低端机型上,GPUImage 有更好的表现。
    • GPUImage 在视频处理上有更好的表现。
    • GPUImage 的代码已经开源。可以根据自己的业务需求,定制更加复杂的管线操作。可定制程度高。

参考文章:

iOS - 图形高级处理 (一、图片显示相关理论)

iOS Rendering 渲染全解析(长文干货)

关于 iOS 离屏渲染的分析与处理

iOS-图片高级处理(三、图片处理实践)

文章来源:https://blog.csdn.net/yueliangmua/article/details/134844693
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。