iOS系统下的图形图像显示原理

常用框架

iOS系统下通常使用UIKit、Core Animation、Core Graphics、Core Image、Core Video等框架来完成图形图像的渲染和显示:

UIKit:UIKit是iOS系统中用于创建用户界面的框架。它提供了许多用于显示文本、图像、按钮等控件的类,同时也提供了一些动画效果的支持。

Core Animation:CoreAnimation是一个动画框架,可以用于创建基于图层的动画。它提供了一些基本的动画类型,比如位移、旋转、缩放等,还支持复杂的动画组合和定时器功能。

Core Graphics:CoreGraphics是一个2D图形框架,可以用于创建和操作图像和路径。它提供了许多绘图函数,可以用于绘制直线、矩形、椭圆、圆弧等形状,还支持颜色、渐变、图案等特效。

Core Image:CoreImage是一个图像处理框架,可以用于对图像进行滤镜、色彩调整、变形等操作。它提供了多种滤镜效果,可以轻松实现图片的美化、修复等功能。

Core Video:CoreVideo是一个视频处理框架,可以用于对视频进行解码、编码、渲染等操作。它提供了多种视频效果,可以用于实现视频编辑、特效制作等功能。

这些框架通常被iOS开发者用来实现图形图像的渲染和显示,同时也可以用于创建各种精美的用户界面和动画效果。

这些框架仍然遵循图形图像渲染pipeline的基本架构,具体的技术栈如图所示:

渲染框架

Core Animation (核心动画)

CoreAnimation

Core Animation

再说说Core Animation,如果你正在编写iOS App,那么无论你是否知道,你都在使用它。Core Animation字面意思是“核心动画”,但它本质上可以理解为一个复合引擎,主要职责包含:渲染、构建和实现动画。

Core Animation最核心的功能是提供给上层框架“UIKit”和“AppKit”使用的Layer对象(iOS系统对应的是CALayer),用来管理和控制屏幕上需要显示的内容(Content),这些内容被分解成独立的Layer,存储在一个叫做Layer Tree(图层树)的层级关系体系之中。于是这个Tree就形成了UIKit以及在iOS应用程序当中你所能在屏幕上看见的一切的基础。

简单来说就是用户能看到的屏幕上的内容都由Layer对象进行管理。Layer对象中的Contents属性保存了由设备渲染流水线渲染好的位图Bitmap(通常也被称为 backing store),而当设备屏幕进行刷新时,会从Layer对象中读取生成好的Bitmap,进而显示到屏幕上。

UIView 和 Layer

在iOS开发过程中,大量使用的视图控件实际上是UIView而不是CALayer,UIView能够呈现可视化内容的原因是UIKit中的每一个UI视图内部都有一个关联的CALayer,用来呈现这些内容。

从上面的层次结构图上来看,UIView处于比Core Animation更高的层级,UIView除了利用Core Animation的CALayer提供可视内容的绘制和动画(Drawing and Animation)能力外,还有其他两个功能:

  • 布局与子视图的管理(Layout and subview management)。
  • 事件处理(Event handling)。

Tree

在UIkit中所有的视图都从一个叫做UIVIew的基类派生而来,视图可以嵌套包含其他视图-子视图,子视图也可以再嵌套包含其他子视图,经过数层的嵌套,就形成一个由不同视图组成的层级关系树,视图树(View Tree),如下图所示。

从上述描述我们知道,每一个UIView都有一个CALayer与之对应,我们把这个对应的Layer叫做UIVIew的Backing Layer。在一个视图树中,每一个View的Layer也跟其View一样组成层级关系树,这个由Layer组成的树叫做图层树(Layer Tree 或 Model Layer Tree)。

View的职责之一就是创建管理这个与它对应的Layer,以确保当SubViews在视图树(View Tree)中添加或者被移除的时候,SubViews关联的Layer也同样在对应的图层树(Layer Tree)中有着相同的操作。

那么为什么iOS要基于UIView和CALayer提供两个平行的层级关系呢,为什么要将 CALayer 独立出来,直接使用 UIView 统一管理不行吗?为什么不用一个统一的对象来处理所有事情呢?

其实这样设计的主要原因就是为了职责分离、功能拆分,方便代码的复用。iOS和MacOS下都可以使用Core Animation框架来负责可视内容的渲染,但是两个系统交互规则不同,所以要将渲染内容和交互分开设计。所以iOS有UIKit和UIView,MacOS则是AppKit和NSView。

实际上并不是只有视图树(View Tree)和图层树(Layer Tree)这两个层级关系树,还有呈现树(Presentation Tree)、渲染树(Render Tree),共四个,每一个Tree都扮演着不同的角色。

渲染树(Render Tree):当UIView对象需要渲染时,它会将自己对应的CALayer对象提交到渲染树中。在渲染树中,CALayer对象会根据自己的层级关系和样式信息进行布局和绘制。渲染树在Core Animation中还有一个重要的作用就是支持动画效果。当View的某个属性发生改变时,Core Animation会自动更新渲染树中对应CALayer对象的属性,并使用硬件加速技术对视图进行动画渲染。

呈现树(Presentation Tree)是渲染树在动画过程中的一个快照,用于描述动画过程中每一帧Layer的状态。当对一个Layer进行动画处理时,系统会根据动画开始和结束时的状态,生成一系列中间状态,这些状态被称为关键帧(Keyframe)。每个关键帧都对应着呈现树中的一个状态,当动画播放时,系统会根据关键帧逐渐改变Layer的状态,从而实现流畅的动画效果。

呈现树的实现原理非常简单,当我们对一个Layer进行动画处理时,系统会自动创建一个呈现树,用于保存Layer在动画过程中的状态。每当动画的状态发生改变时,系统就会更新呈现树中对应CALayer对象的属性,从而反映出视图的当前状态。呈现树不会影响视图(UIView)的布局和绘制过程,它仅仅用于保存Layer在动画过程中的状态,所以它的计算成本非常小,可以实现高效的动画渲染。

Core Animation(渲染管线)

Core Animation使用了一个基于GPU的渲染管线来呈现View和Layer,并通过优化和预合成来提高性能,我们把这个过程叫做Core Animation渲染管线。

pipeline

Core Animation渲染管线是从App开始,App内构建了视图层次结构视图层次结构可以是View Tree,也可以是直接使用Core Animation构建的的Layer Tree。

App不直接通过Core Animation做图像的渲染工作,而是将上述的视图层次结构数据打包提交给Render Server(渲染服务器)。Render Server是系统中一个独立的进程,App使用IPC(进程间通信)的方式与Render Server进行通信。Core Animation框架分为客户端和服务器版本,App内使用的是Core Animation框架的客户端版本,Render Server使用的是Core Animation服务器版本。

Render Server收到视图层次结构数据后,Core Animation的服务端利用OpenGL/Metal渲染视图层次结构数据,当然渲染工作是在GPU中运行的。

视图层次结构数据渲染完成后,就可以通过显示器(Display)将其显示给用户。

Render Loop (渲染循环)

Render Loop(渲染循环)是iOS系统中渲染图形的核心循环,Render Loop不间断的捕获用户屏幕触摸事件,将事件传递给系统分析后,使用Core Animation渲染管线来更新相应场景状态和渲染显示相应图形,确保应用程序的图形表现能够实时响应用户操作,并以流畅的方式呈现在屏幕上。

RenderLoop

RenderLoopOne

一个Core Animation渲染管线主要涉及以下几个步骤:

Handle Events(事件处理):

应用程序接收到iOS系统传递个它的事件(触摸(Touch)、网络回调(Networking)、键盘操作(Keyboard)、计时器(Timers)),应用程序根据事件类型执行对应操作,比如创建和调整视图层级、设置视图的Frame、修改背景颜色、添加一个动画等。

Events

这些操作最终都会被CALayer标记,并通过Core Animation提交到一个中间状态中去。注意这时候对视图或者图层属性的修改还没生效,只是打上标记,意思是需要更新。开发者也可以使用setNeedsLayout来手动标记。

setNeesLayout

Commit Transaction (提交事务):

在Application的阶段的最后一个一步是Commit Transaction,也是将呈现树(Presentation Tree)打包发送给Render Server前的最后一步。Commit Transcation其实可以细分为 4 个步骤:布局(Layout)、显示(Display)、准备(Prepare)、提交(Commit)。
LayoutAndDrawViews

布局(Layout):

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

  • 调用View里面重载的layoutSubviews方法;
  • 创建View,并通过addSubview方法添加SubView;
  • 计算View的布局,自动布局约束的计算等。

一旦计算出布局,系统就会调用setNeedsDisplay方法,标记View需要更新。

显示(Display):

这个阶段是交给Core Graphics的CGContext进行视图的绘制,并不是在显示器上显示,主要绘制以下内容:

  • 如果开发者重写了drawRect:方法,那么系统会调用重载的drawRect:方法,在这个方法中绘制图形,绘制的图形bitmap数据保存在内存中。
  • 使用Core Text或者Core Graphics进行文字的绘制。

这个阶段仍然使用CPU和系统内存进行绘制,还没使用到显卡。

准备(Prepare)

这阶段Core Animation准备将动画数据发送到渲染服务器,一般进行图像的解码和转换等操作。

  • 如果视图或者子视图里面包含图像显示,将会进行图像的解码;
  • 如果视图或者子视图里面包含图像显示,但是这种图像格式不被GPU不支持,那么执行图像转换操作。

提交(Commit)

这阶段将图层进行打包发送给Render Server。该过程会执行递归图层树,所以如果图层数太复杂,此阶段产生更大的消耗。

以下阶段就是在Render Server进程中执行了。

Decode(解码):

当上述打包好的图层数据被传输到Render Server之后,首先会进行Decode(解码)操作。就是将打包的图层数据解码成Render Server能够读取的视图层次结构。完成解码之后需要等待下一次垂直同步信号(VSync)后才会执行下一步Draw Calls操作。 这个等待是为了保证上一个帧缓冲区的内容能够被完整的渲染显示。

Draw Calls(绘制回调):

当接收到下一次垂直同步信号(VSync)后,Render Server开始对GPU(在底层使用OpenGL 或Metal)发出绘制调用。

Render(渲染)

当收到Render Server绘制调用后,会立即开始渲染阶段的操作。渲染阶段的操作可以分为两个部分,准备(Render prepare)和执行(Render execute)。

  • 准备渲染(Render prepare):准备图层树,准备运行GPU渲染管线的绘制命令。根据事务提交阶段的时间通过插值计算页面上的动画当前帧的状态;将图层和特效分解为一步步简单操作。
  • 执行渲染(Render execute):这阶段使用GPU的渲染管线进行一步步的图形图像绘制操作,最终生成图形以供下一步显示。

理想的状态是在收到下一个垂直同步信号(VSync)之前,就需要完成当前所有的渲染操作,包括帧缓冲区的交换,因为我们希望在收到下一个垂直同步信号(VSync)后能够立即展示帧缓冲区中的渲染后的图像。

Display(显示)

此处的Display(显示)和Commit Transaction阶段的Display不一样,这个阶段的Display就是显示器从准备好的帧缓冲区逐步取出像素信息展示在显示器上。

以上各个阶段的操作并不能在一个同步周期(两个垂直同步信号之间的时间)内一次性完成,苹果为了优化渲染管线流程,提高图形图像的渲染效率,将上述各个阶段的操作分散到多个同步周期中,所以Render Loop(渲染循环)中的多个渲染管线的时间是并行的交织在一起的。入下图,当CPU正在读取第N帧时,此时GPU正在渲染前一帧(第N-1帧),显示器正在显示N-2帧,如此交替往复就形成了Render Loop。

TheRenderLoop

Offscreen Rendering(离屏渲染)

我们都知道,在屏幕中显示内容时,通常需要使用至少一个帧缓冲区(Frame Buffer)来存储像素数据,这是GPU用来渲染和存储最终图像的地方。GPU会将渲染结果直接写入帧缓冲区(Frame Buffer),然后显示在屏幕上。

然而,在某些情况下,可能会有一些限制,使得不能或者不需要直接将渲染结果写入帧缓冲区(Frame Buffer)。这些限制可能是由于特定的渲染效果、开发者主动指定的光栅化。在这种情况下,渲染结果需要先存储到一个单独的内存区域--离屏缓冲区(Offscreen Buffer),然后等到合适的时机再将其写入帧缓冲区(Frame Buffer)。这个过程就被称为离屏渲染(Offscreen Rendering)。

离屏渲染可以在不直接显示在屏幕上的情况下进行图像处理和渲染操作。通过将渲染结果存储在离屏缓冲区中(Offscreen Buffer),可以进行后续的处理、读取或传输。一些常见的用途包括图像后处理、渲染到纹理(Render-to-texture)以及生成图像数据供后续使用。

但是离屏渲染会增加渲染操作的延迟。一方面离屏渲染执行过程中会使得GPU不断在帧缓冲区(Frame Buffer)和离屏缓冲区(Offscreen Buffer)之间进行上下文切换,这种切换的代价非常大;另一方面离屏渲染需要额外的内存读写操作,也需要更多的内存来存储渲染结果,所以大量的离屏渲染会内存压力过大。

离屏渲染的用途

离屏渲染可能带性能问题,那为什么还要使用离屏渲染技术呢?

  • 一些特殊渲染效果(比如Shadows、Masks, Rounded Rectangles、 Visual Effects等)需要使用离屏缓冲区来保存渲染的中间状态,所以不得不使用离屏渲染。
  • 为了效率,开发者可以将内容提前渲染保存在离屏缓冲区中(开启shouldRasterize光栅化),后续如果使用直接从离屏缓冲区读取即可,不需要重复渲染。

Hitch(可能出现的性能问题)

通过上述的介绍,我们知道Render Loop的主要工作都是在CPU和GPU中进行。CPU和GPU是并行进行工作的,当CPU正在处理第N帧时,GPU正在渲染前一帧(第N-1),依此类推。所以,CPU和GPU不能分别在一帧时间内完成对应的工作,都会延迟下一帧的处理、渲染和显示,用户界面就会出现卡顿现象。

gpu和cpu

Commit hitch(提交故障)

在Commit Transaction(提交事务)阶段应用程序可能需要花费更多时间来处理事务,导致应用程序不得不延迟向Render Server(渲染服务器)提交下一帧的图层数据,此阶段发生的故障叫做Commit hitch(提交故障)。

commithitch1

commithitch2

发生Commit hitch主要是在Commit hitch这个阶段主线程任务过重,可以通过以下方法减轻主线程的负担。

尽可能的保持View轻量:

  • 减少drawRect:方法的复杂度,如果CALayer的方法能够替换自定义绘制的内容尽量使用CALayer方法。
  • 如果用不到drawRect:,就不要重写drawRect:方法。
  • 避免频繁的创建和销毁视图,可以使用重用机制复用视图。
  • 可以使用隐藏属性替代频繁的移除和插入视图。

减少负担重或者冗余的布局:

  • 使用setNeedsLayout方法来刷新视图布局而不是使用layoutIfNeeded方法。
  • 尽量减少约束的使用。
  • 尽量减少递归布局的使用。

Render hitch(渲染故障)

上面提到Render阶段有两部分工作:渲染准备和渲染执行。如果这两部分的工作中的任一工作都没有及时的在一帧的时间内完成,都会造成Render hitch(渲染故障)。

wwdc21-HitchesRenderPhaseHitch1

其中大量的离屏渲染工作是造成Render hitch的一个重要原因,下面几种情况会触发离屏渲染:

  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 等)

所以如果开发者要避免发生Render hitch,就要尽量避免触发主动或者被动离屏渲染操作。


参考文档:
1、《iOS 页面渲染 - 流程》
https://juejin.cn/post/7038170936163450910
2、《iOS下的渲染框架》
https://zhuanlan.zhihu.com/p/157556221
3、《iOS Rendering 渲染全解析》
https://github.com/RickeyBoy/Rickey-iOS-Notes/blob/master/%E7%AC%94%E8%AE%B0/iOS%20Rendering.md
4、《Core Animation Basics》
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreAnimation_guide/CoreAnimationBasics/CoreAnimationBasics.html
5、《Understanding UIKit Rendering》
https://www.wwdcnotes.com/notes/wwdc11/121/
6、《WWDC 2021: Avoid hitches and discover the Render Loop》
https://a11y-guidelines.orange.com/en/mobile/ios/wwdc/nota11y/2021/21hitches/
7、《Оптимизация рендера в iOS: frame buffer, Render Server, FPS, CPU vs GPU》
https://habr.com/ru/post/647177/
8、《Rendering performance of iOS apps》
https://dmytro-anokhin.medium.com/rendering-performance-of-ios-apps-4d09a9228930
9、《Advanced Graphics and Animations for iOS Apps》
https://www.wwdcnotes.com/notes/wwdc14/419/
10、《Core Animation Essentials》
https://www.wwdcnotes.com/notes/wwdc11/421/
11、《iOS Core Animation: The Layer Tree》
https://www.informit.com/articles/article.aspx?p=2128062

分类: 音视频开发iOS开发 标签: OpenGLMetal管线缓冲区Xcode

评论

暂无评论数据

暂无评论数据

目录