[iOS开发]渲染相关问题

Posted Billy Miracle

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[iOS开发]渲染相关问题相关的知识,希望对你有一定的参考价值。

首先我们要了解一些基础知识:
计算机图形渲染原理
移动终端屏幕成像与卡顿

ios的各个渲染框架以及iOS图层渲染原理

(一)渲染技术栈


在硬件基础之上,iOS 中有 Core Graphics、Core Animation、Core Image、OpenGL 等多种软件框架来绘制内容,在 CPU 与 GPU 之间进行了更高层地封装。

(二)渲染技术栈的概念说明

①-应用交互前端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

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 进行管理。

2. CALayer 是显示的基础:存储 bitmap「由bitmap可以联系到渲染过程」

简单理解,CALayer 就是屏幕显示的基础。在 CALayer.h 中,CALayer 有这样一个属性 contents :

contents 提供了 layer 的内容,是一个指针类型,在 iOS 中的类型就是 CGImageRef(在 OS X 中还可以是 NSImage)。CGImageRef的定义是:A bitmap image or image mask
实际上,CALayer 中的 contents 属性保存了由设备渲染流水线渲染好的位图 bitmap(通常也被称为 backing store),而当设备屏幕进行刷新时,会从 CALayer 中读取生成好的 bitmap,进而呈现到屏幕上。

// typedef struct CF_BRIDGED_TYPE(id) CGImage *CGImageRef;
myView.layer.contents = (__bridge id)[UIImage imageNamed:@"1.jpg"].CGImage;

在运行时,操作系统会调用底层的接口,将 image 通过 CPU+GPU 的渲染流水线渲染得到对应的 bitmap,存储于 CALayer.contents 中,在设备屏幕进行刷新的时候就会读取 bitmap 在屏幕上呈现。
也正因为每次要被渲染的内容是被静态的存储起来的,所以每次渲染时,Core Animation 会触发调用 drawRect: 方法,使用存储好的 bitmap 进行新一轮的展示。

(三)UIView 与 CALayer 的关系

1. UIView的职责

UIView 是 app 中的基本组成结构,定义了一些统一的规范。它会负责内容的渲染以及,处理交互事件。具体而言,它负责的事情可以归为下面三类:

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

2. CALayer的职责

CALayer 的主要职责是管理内部的可视内容。当我们创建一个 UIView 的时候,UIView 会自动创建一个 CALayer,为自身提供存储 bitmap 的地方(也就是前文说的 backing store),并将自身固定设置为 CALayer 的代理。

3. 总结对比

  • CALayerUIView 的属性之一,负责渲染和动画,提供可视内容的呈现。
  • UIView 提供了对 CALayer 部分功能的封装,同时也另外负责了交互事件的处理
    • 为什么 UIKit 中的视图能够呈现可视化内容?就是因为 UIKit 中的每一个 UI 视图控件其实内部都有一个关联的 CALayer,即 backing layer
    • CALayer 事实上是用户所能在屏幕上看见的一切的基础

4. 一些面试考点

  • 层级结构:我们对 UIView 的层级结构非常熟悉,由于每个 UIView 都一一对应了CALayer 负责页面的绘制,所以视图层级拥有视图树的树形结构,对应 CALayer 层级也拥有图层树的树形结构。
    其中,视图的职责是 创建并管理 图层,以确保当子视图在层级关系中 添加或被移除 时,其关联的图层在图层树中也有相同的操作,即保证视图树和图层树在结构上的一致性。
  • 部分效果的设置: 因为 UIView 只对 CALayer 的部分功能进行了封装,而另一部分如圆角、阴影、边框等特效都需要通过调用 layer 属性来设置。
  • 是否响应点击事件CALayer 不负责点击事件,所以不响应点击事件,而 UIView 会响应。
  • 不同继承关系CALayer 继承自 NSObjectUIView 由于要负责交互事件,所以继承自 UIResponder。因而可以子线程绘制layer来显示。

5. iOS提供UIView与CALayer两个平行层级的原因

为什么要将 CALayer 独立出来,直接使用 UIView 统一管理不行吗?为什么不用一个统一的对象来处理所有事情呢?

  • 这样设计的主要原因就是为了职责分离,拆分功能,方便代码的复用;
  • 通过 Core Animation 框架来负责可视内容的呈现,这样在 iOS 和 OS X 上都可以使用 Core Animation 进行渲染;
  • 与此同时,两个系统还可以根据交互规则的不同来进一步封装统一的控件,比如 iOS 有 UIKitUIView,OS X 则是AppKitNSView
  • 实际上,这里并不是两个层级关系,而是四个。每一个都扮演着不同的角色。除了视图树图层树,还有呈现树渲染树

(四)CALayer显示可视化内容的原理

为什么 CALayer 可以呈现可视化内容呢?因为 CALayer 基本等同于一个 纹理。纹理是 GPU 进行图像渲染的重要依据。
在 计算机图形渲染原理 中提到纹理本质上就是一张图片,因此 CALayer 也包含一个 contents 属性指向一块缓存区,称为 backing store,可以存放位图(Bitmap)。iOS 中将该缓存区保存的图片称为 寄宿图

图形渲染流水线支持从顶点开始进行绘制(在流水线中,顶点会被处理生成纹理),也支持直接使用纹理(图片)进行渲染。相应地,在实际开发中,绘制界面也有两种方式:一种是 手动绘制;另一种是 使用图片。
对此,iOS 中也有两种相应的实现方式:

  • 使用图片:contents image
  • 手动绘制:custom drawing

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 数据。

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 会创建一个空的寄宿图(尺寸由 boundscontentScale 决定)和一个 Core Graphics 的绘制上下文,为绘制寄宿图做准备,作为 ctx 参数传入
    - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;
    
  • 最后,由 Core Graphics 绘制生成的寄宿图会存入 backing store

(五)Core Animation 渲染全内容

1. Core Animation Pipeline 渲染流水线

(1)Core Animation 渲染流水线的工作原理

当我们了解了 Core Animation 以及 CALayer 的基本知识后,我们知道了 CALayer 的本质,那么它是如何调用 GPU 并显示可视化内容的呢?下面介绍一下 Core Animation 渲染流水线的工作原理。

  • 事实上,app 本身并不负责渲染,渲染则是由一个独立的进程负责,即 Render Server 进程
  • App 通过 IPC 将渲染任务及相关数据提交给 Render Server
  • Render Server 处理完数据后,再传递至 GPU
  • 最后由 GPU 调用 iOS 的图像设备进行显示

(2)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 刷新率的支持,需要将这些步骤进行分解,通过流水线的方式进行并行执行:

2. Commit Transaction 发生了什么

一般开发当中能影响到的就是 Handle Events 和 Commit Transaction 这两个阶段,这也是开发者接触最多的部分。Handle Events 就是处理触摸事件;
在 Core Animation 流水线中,app 调用 Render Server 前的最后一步 Commit Transaction 其实可以细分为 4 个步骤:

  • Layout
  • Display
  • Prepare
  • Commit

(1)Layout(构建视图)

Layout 阶段主要进行视图构建和布局,具体步骤包括:

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

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

(2)Display(绘制视图)

  • 这个阶段主要是交给 Core Graphics 进行视图的绘制,注意不是真正的显示,而是得到前文所说的图元 primitives 数据:
    • 根据上一阶段 Layout 的结果创建得到图元信息。
    • 如果重写了 drawRect: 方法,那么会调用重载的 drawRect: 方法,在 drawRect: 方法中手动绘制得到 bitmap 数据,从而自定义视图的绘制
  • 注意正常情况下 Display 阶段只会得到图元 primitives 信息,而位图 bitmap 是在 GPU 中根据图元信息绘制得到的
  • 但是如果重写了 drawRect: 方法,这个方法会直接调用 Core Graphics 绘制方法得到 bitmap 数据,同时系统会额外申请一块内存,用于暂存绘制好的 bitmap;
  • 由于重写了 drawRect: 方法,导致绘制过程从 GPU 转移到了 CPU,这就导致了一定的效率损失;
  • 与此同时,这个过程会额外使用 CPU 和内存,因此需要高效绘制,否则容易造成 CPU 卡顿或者内存爆炸

(3)Prepare(Core Animation 额外的工作)

Prepare 阶段属于附加步骤,一般处理图像的解码和转换等操作。

(4)Commit(打包并发送)

  • 这一步主要是:将图层打包并发送到 Render Server
  • 注意 commit 操作是依赖图层树递归执行的,所以如果图层树过于复杂,commit 的开销就会很大
  • 这也是我们希望减少视图层级,从而降低图层树复杂度的原因

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 操作使用

iOS动画染原理

iOS动画处理的三个阶段

iOS 动画的渲染也是基于上述 Core Animation 流水线完成的。这里我们重点关注 app 与 Render Server 的执行流程。
日常开发中,如果不是特别复杂的动画,一般使用 UIView Animation 实现,iOS 将其处理过程分为如下三部阶段:

  • Step 1:调用 animationWithDuration:animations: 方法
  • Step 2:在 Animation Block 中进行 Layout,Display,Prepare,Commit 等步骤。
  • Step 3:Render Server 根据 Animation 逐帧进行渲染

iOS OffScreen Rendering离屏渲染原理

(一)离屏渲染具体过程

1. 通常的渲染

简化来看,通常的渲染流程是这样的:

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

2. 离屏渲染

离屏渲染的流程是这样的:

与普通情况下 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 中,达到复用的目的。

1. 被动触发

对于第一种情况,也就是不得不使用离屏渲染的情况,一般都是系统自动触发的,比如阴影、圆角等等。
最常见的情形之一就是:使用了 mask 蒙版:

由于最终的内容是由两层渲染结果叠加,所以必须要利用额外的内存空间对中间的渲染结果进行保存,因此系统会默认触发离屏渲染。
又比如下面这个例子,iOS 8 开始提供的模糊特效 UIBlurEffectView

整个模糊过程分为多步:

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

2. 主动使用

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

(四)shouldRasterize 光栅化

  • 开启光栅化后,会触发离屏渲染,Render Server 会强制将 CALayer 的渲染位图结果 bitmap 保存下来,这样下次再需要渲染时就可以直接复用,从而提高效率
  • 而保存的 bitmap 包含 layer 的 subLayer、圆角、阴影、组透明度 group opacity 等,所以如果 layer 的构成包含上述几种元素,结构复杂且需要反复利用,那么就可以考虑打开光栅化
  • 圆角、阴影、组透明度等会由系统自动触发离屏渲染,那么打开光栅化可以节约第二次及以后的渲染时间。而多层 subLayer 的情况由于不会自动触发离屏渲染,所以相比之下会多花费第一次离屏渲染的时间,但是可以节约后续的重复渲染的开销
  • 不过使用光栅化的时候需要注意以下几点:
    • 如果 layer 不能被复用,则没有必要打开光栅化
    • 如果 layer 不是静态,需要被频繁修改,比如处于动画之中,那么开启离屏渲染反而影响效率
    • 离屏渲染缓存内容有时间限制,缓存内容 100ms 内如果没有被使用,那么就会被丢弃,无法进行复用
    • 离屏渲染缓存空间有限,超过 2.5 倍屏幕像素大小的话也会失效,无法复用

(五)圆角的离屏渲染

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

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

view.layer.cornerRadius = 20

上述代码只会对background colorborder of the layer起作用, 但是对layer.content 并不起作用,就是说不会设置 content 的圆角,除非同时设置了 layer.masksToBoundstrue(对应 UIView 的 clipsToBounds 属性)。
如果只是设置了 cornerRadius 而没有设置 masksToBounds,由于不需要叠加裁剪,此时是并不会触发离屏渲染的。而当设置了裁剪属性的时候,由于 masksToBounds 会对 layer 以及所有 subLayercontent 都进行裁剪,所以不得不触发离屏渲染

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

所以,在没有必要使用圆角裁剪的时候,我们应该尽量不去触发离屏渲染而影响效率。

(六)离屏渲染的具体逻辑

刚才说了圆角加上 masksToBounds 的时候,因为 masksToBounds 会对 layer 上的所有内容进行裁剪,从而诱发了离屏渲染,那么这个过程具体是怎么回事呢,下面我们来仔细讲一下。
图层的叠加绘制大概遵循“画家算法”,在这种算法下会按层绘制,首先绘制距离较远的场景,然后用距离较近的场景覆盖较远的部分。

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

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

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

(七)避免圆角离屏渲染

  • 除了尽量减少圆角裁剪的使用,还有什么别的办法可以避免圆角+裁剪引起的离屏渲染吗?
  • 由于刚才我们提到,圆角引起离屏渲染的本质是裁剪的叠加,导致 masksToBoundslayer 以及所有 sublayer 进行二次处理。那么我们只要避免使用 masksToBounds 进行二次处理,而是对所有的 sublayer 进行预处理,就可以只进行“画家算法”,用一次叠加就完成绘制
  • 那么可行的实现方法大概有下面几种:
    1. 【换资源】直接使用带圆角的图片,或者替换背景色为带圆角的纯色背景图,从而避免使用圆角裁剪。不过这种方法需要依赖具体情况,并不通用。
    2. 【mask】再增加一个和背景色相同的遮罩 mask 覆盖在最上层,盖住四个角,营造出圆角的形状。但这种方式难以解决背景色为图片或渐变色的情况。
    3. 【UIBezierPath】用贝塞尔曲线绘制闭合带圆角的矩形,在上下文中设置只有内部可见,再将不带圆角的 layer 渲染成图片,添加到贝塞尔矩形中。这种方法效率更高,但是 layer 的布局一旦改变,贝塞尔曲线都需要手动地重新绘制,所以需要对 framecolor 等进行手动地监听并重绘。
    4. 【CoreGraphics】重写 drawRect:,用 CoreGraphics 相关方法,在需要应用圆角时进行手动绘制。不过 CoreGraphics 效率也很有限,如果需要多次调用也会有效率问题。

(八)触发离屏渲染原因的总结

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

  • 使用了 mask 的 layerlayer.mask
  • 需要进行裁剪的 layerlayer.masksToBounds / view.clipsToBounds
  • 设置了组透明度为 YES,并且透明度不为 1 的 layer (layer.allowsGroupOpacity/layer.opacity
  • 添加了投影的 layer (layer.shadow
  • 采用了光栅化的 layer (layer.shouldRasterize
  • 绘制了文字的 layer (UILabelCATextLayerCore Text 等)

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

以上是关于[iOS开发]渲染相关问题的主要内容,如果未能解决你的问题,请参考以下文章

探究光栅图像学之水纹渲染与折射滤镜

[iOS开发]渲染相关问题

[iOS开发]渲染相关问题

渲染管道光栅阶段四“片元着色器”

渲染管道光栅阶段四“片元着色器”

iOS 离屏渲染问题