【iOS】渲染原理离屏渲染
渲染原理&&离屏渲染
- 图像渲染流程
- Application
- Geometry
- Resterization
- Pixel像素处理阶段:处理像素,得到位图
- 图像渲染技术栈
- UIKit
- Core Animation
- Core Graphics
- Core Image
- OpenGL ES
- Metal
- UIView与CALayer的关系
- CALayer
- contents image
- contents Drawing
- Core Animation流水线
- Commit Transation
- Layout:构建视图
- Display:绘制视图
- prepare:Core Animation额外的工作
- commit:打包并发送
- Rendering Pass:Render Server的具体操作
- 离屏渲染
- 离屏渲染产生的原因
- 圆角的离屏渲染
- 触发离屏渲染的几种情况
- 离屏渲染对性能的影响
- 离屏渲染的优化策略
图像渲染流程
这里只有Application
阶段是由CPU负责,后续都是由GPU负责
Application
该阶段目的主要是得到图元,指的是图像在应用中被处理的阶段,此时还处于CPU负责的时期,在这个阶段可以会对图像进行一系列的操作或者改变,最终将新的图像信息传给下一个阶段。这部分信息被叫做图元
[!TIP]
图元就是描述几何形状的基本元素,通常为三角形、线段、顶点等
Geometry
该阶段主要是几何处理阶段,从这个阶段开始,包括以后的阶段都是主要由GPU负责了。这个时候GPU可以拿到上一个阶段传递下来的图元信息,GPU会对这部分图元通过顶点着色器、形状装配、几何着色器进行处理,之后输出新的图元。这一系列阶段包括:
- 顶点着色器:该阶段中会将图元中的顶点信息进行视角的转换,添加光照信息、增加纹理等操作
- 形状装配:图元中的三角形、线段、点分别对应三个
Vertex
、两个Vertex
、一个Vertex
。这个阶段会将Vertex
连接成相对应的形状 - 几何着色器:额外添加额外的
Vertex
,将原始图元转换成新的图元,以构建一个不一样的模型。简单的说就是基于通过三角形、线段和点构建成更负责的几何图形。
Resterization
光栅化的主要目的是将几何渲染之后的图元信息,转换为一系列的像素,以便后续显示在屏幕上
工作原理:根据图元信息,计算出每个图元所覆盖的像素信息等,从而将像素划分成不同的部分
以本图为例:中心点若是在图元内部,那么这个像素就属于这个图元。
Pixel像素处理阶段:处理像素,得到位图
在上面的光栅化处理之后,我们得到了图元对应的像素,之后我们需要通过片段着色器(给每一个像素Pixel赋予正确的颜色)和测试与混合(处理片段的前后位置以及透明度)给这些像素填充正确的颜色和小锅得到位图以便最终显示在屏幕上。
而经过了处理、蕴含着大量信息的像素点集合,就被称作位图
- 片段着色器:**该阶段的目的是给每一个像素Pixel赋予正确的颜色。**颜色的来源就是之前得到的顶点、纹理、光照等信息。这里需要处理纹理、光照等复杂信息,所以这通常是整个系统的性能瓶颈。
- 测试和混合:这个阶段主要的目的是处理片段的前后位置以及透明度,这个阶段会检测各个做色片段的深度值z坐标,以判断片段的墙后位置,以及是否应该被舍弃。同时也会计算响应的透明度,进行片段的混合,得到最终的颜色。
图像渲染技术栈
整个图像渲染技术栈:APP使用Core Graphics
、Core Animation
、Core Image
等框架来绘制可视化内容,这些软件框架之间相互依赖。这些框架都需要通过OpenGL
来调用GPU绘制,最终将内容显示到屏幕之上
UIKit
UIKit
框架提供了iOS所需的基本架构,提供了用于实施界面的窗口和视图架构,用于向APP提供多点触控和其他类型输入的时间处理基础架构,以及管理用户、系统、app之间互动所需的主运行循环。
Core Animation
这是一个复合引擎,职责是尽可能快地组合屏幕上不同的可视内容,这些可视内容可被分解成独立的图层,这些图层会被存储在一个叫做图层树的体系之中。本质来说,CALayer
是用户所能在屏幕上看见一切的基础。
Core Graphics
这是基于Quartz
高级绘图引擎,主要用于运行时绘制图像。开发者可以使用这个框架来处理基于路径的绘图、转换、颜色管理、离屏渲染、图案、渐变和阴影,图像数据管理,图像创建和图像遮罩以及PDF文档创建,显示和分析。
当我们需要运行时创建图像的时候,可以使用Core Graphics
去绘制。与之相对应的是运行前创建图像。
Core Image
Core Image
和Core Graphics
相反,用于处理运行前创建的图像。其拥有一系列线程的图像过滤器,能对已存在的图像进行高效的处理。大多数情况,Core Image
会在GPU中完成工作。
OpenGL ES
这是OpenGL
的子集。而OpenGL
是一套第三方标准,函数内部的实现由对应的GPU厂商开发实现
Metal
Metal
类似于OpenGL ES
,这也是一套第三方标准,具体由Apple实现。开发者都在间接的使用Metal
。Core Animation
、Core Image
、SceneKit
、SpriteKit
等等渲染框架都是构建于 Metal
之上的。
UIView与CALayer的关系
CALayer
事实上是用户所能在屏幕上看见的一切的基础。对于UIKit
来说,每一个UI视图空间内部其实都有一个关联的CALayer
,正是这种一一对应的关系,视图层级拥有视图树的树形结构,对应CALayer
层级也拥有图层树的树形结构:
为什么iOS要基于UIView和CALayer提供两个平行的层级关系?
这里的原因是要做到职责分离,这样能避免有很多重复的代码。
CALayer
CALayer
等同于一个纹理。纹理是GPU进行图像渲染的重要依据。纹理本质上来说就是一张图片,故而CALayer
包含一个contents
属性指向一块缓存区,称作backing store
,可以存放位图。iOS中将该缓存区保存的图片成为寄宿图。
在实际开发中,iOS有两种相应的方式去绘制界面
- 使用图片:contents image
- 手动绘制:custom drewing
contents image
这是指CALayer的contents属性来设置图片。本质上,contents
属性指向的一块缓存区域,称为backing store,可以存放bitmap数据。
contents Drawing
这是指使用Core Graphics
来直接绘制寄宿图。在实际开发中,一般通过继承UIview
实现drawRect:
方法来自自定义绘制。
虽然drawRect:
是一个UIview
方法,但是事实上都是底层的CALayer
完成了重绘工作并且保存了图片。这里大概展示一个drawRect
绘制定义寄宿图的基本原理。
Core Animation流水线
APP本身其实并不负责渲染,渲染是由一个独立的进程负责,即Render Server
进程。
APP通过IPC将渲染任务以及相关的数据交给1Render Server
,其处理完数据以后,再传递至GPU。最后由GPU调用iOS图像设备进行显示。
Core Animation流水线的工作流程:
- Handle Events:APP响应事件,改变视图/图层,触发渲染流水线开始
- Commit Transaction:在CPU上处理显示内容的前置计算,包含布局、显示、准备、提交四个阶段,发生在应用程序进程中。
- Decode:打包好的图层被传输到
Render Server
,之后,首先进行解码。注意完成解码之后需要等待下一个Runloop
才会执行,下一步Draw Calls
。 - Render:这是由GPU进行渲染
- DIsplay:显示阶段,需要等
render
结束下一个Runloop的触发显示
对上述步骤串联,执行所消耗的事件超过了16.67ms,因此为了满足屏幕的60FPS刷新率的支持,需要将这些步骤进行分解,通过流水线的方式进行并线执行,下图所示:
Commit Transation
在Core Animation
流水线中,app调用Render Server
前的最后一步Commit Transation
其实可以细分4个步骤:
Layout:构建视图
在该阶段主要处理视图构建和布局,具体步骤包括以下几步:
- 调用重载的
layoutSubviews
方法 - 创建视图,通过
addSubview
方法添加子视图 - 计算视图布局,所有的
Layout Constraint
Display:绘制视图
在该阶段主要是交给Core Graphics
进行视图的绘制以得到图元数据:
- 根据俄上个阶段
Layout
的结果创建得到图元信息 - 重写
drawRect:
方法,会调用重载的drawRect
方法,在drawRect
方法中手动绘制得到bitmap
数据,从而自定义视图的绘制
正常情况下Display
阶段这能得到图元信息,但是重写了drawRect
方法,这个方法会直接调用Core Graphics
绘制方法得到bitmap数据,同时系统会额外申请一块内存,去保存暂存的bitmap。
由于我们重写了drawRect:
方法,导致绘制过程从GPU转移到了CPU,这会导致一定的效率损失。这个时候,这个过程会额外使用CPU和内存,因此需要高效绘制,否则容易造成CPU卡顿或者内存爆炸。
prepare:Core Animation额外的工作
主要进行图片解码和转换
commit:打包并发送
这一步主要是:图层打包并发送Render Server
注意commit
操作是依赖图层树递归执行的,所以如果图层树过于复杂,commit
的开销就会很大,这也是为什么我们希望减少视图层级,降低图层树复杂度的原因。
Rendering Pass:Render Server的具体操作
Render Server
通常是OpenGL
或者是Metal
。
若是OpenGL
的话,那么上图中主要是GPU中执行的操作,具体主要包括:
- GPU收到包括图元信息的
Command Buffer
- Tiler开始工作:先通过顶点着色器
Vertex Shader
对顶点进行处理,更新图元信息。 - 平铺过程:平铺生成
title bucket
几何图形,这一步会将图元信息转化为像素,之后再将结果写入Paramerter buffer
中 Tiler
更新完所有的图元信息,或者Parameter Buffer
已经满了,则会开始下一步Renderer
工作:将像素信息进行处理得到bitmap
,之后存入Render Buffer
Render BUffer
中存储有渲染好的bitmap
,供之后的display操作使用
离屏渲染
简单来说,如果不能一次性得到渲染结果,必须要一次或者多次渲染将结果缓存存入到内存区域,最后再结合后写入到Frame Buffer
,这样的渲染就被称为离屏渲染
离屏渲染产生的原因
iOS中主要的渲染操作都是由Render Server
模块通过调用显卡驱动所提供的OpenGL/Metal
接口来执行的。通常对于每一层layer,Render Server
会遵循画家算法,按次序输出到frame Buffer
,之后一层一层的覆盖,就能得到最终的显示结果。
有些场景没有那么简单。作为画家的GPU一层一层的向画布上进行输入,但是其没有办法在某一层渲染完成之后回过头去更改某一部分,这是由于这一层之前的若干层layer像素数据,已经在渲染中被永久覆盖了。这也就意味着对于每一次layer而言,要么找到一种通过单次遍历就能完成渲染的算法,要么就不得不另开一块内存,借助这个临时中转区域来完成一些更复杂,多次修改/裁剪操作。
圆角的离屏渲染
若是只设置了layer
的cornerRadius
而没有设置masksToBounds
,由于不需要叠加裁剪,这个时候是不会触发离屏渲染。而当设置了裁剪属性的时候,由于masksToBounds
会对layer以及所有的subLayer
的content
都进行裁剪,所以不得不触发离屏渲染
在普通的layer绘制中,上层的sublayer会覆盖下层的sublayer,下层的sublayer绘制完成之后就可以抛弃了。所有sublayer
依次绘制完毕之后,整个绘制过程完成,就可以进行后续的呈现了。
但是设置了cornerRadius
以及masksToBounds
进行圆角裁剪的时候,masksToBounds
裁剪属性会应用到所有sublayer
上面,也就是说所有的sublayer
在第一次被绘制之后不能立刻被丢弃还需要保存在Offscreen buffer
中等待下一轮圆角 +裁剪,这就会导致离屏渲染。
触发离屏渲染的几种情况
- 使用了mask的layer(
layer.mask
) - 需要进行裁剪的layer(
layer.masksToBounds
/view.clipsToBounds
) - 设置了组透明度为YES,并且透明度不为1的layer(
layer.allowsGroupOpacity
/layer.opacity
) - 添加了投影的layer(
layer.shadow
) - 采用了光栅化的layer(
layer.shouldRasterize
) - 绘制了文字的layer(
UILabel
、CATextLayer
、Core Text
)
离屏渲染对性能的影响
首先,明确离屏渲染会增大GPU的负担
- GPU的工作是高度流水线化的,不断向
Frame Buffer
输出,若是出现离屏渲染的情况,则GPU流水线不得不先切换上下文,将中间的结果输出到Offscreen Buffer
,等到中间过程完成后再组成所有中间结果输出到Frame Buffer
。如果GPU频繁切换上下文,将严重影响GPU的情况,导致掉帧的情况。 - 离屏渲染需要额外的存储空间
离屏渲染的优化策略
- 避免不必要的圆角:对于静态内容,可以预先将图片裁剪为带圆角的版本;对于动态内容,考虑使用
UIBezierPath
或者Core Graphics
绘制圆角;或者使用maskView
代替cornerRadius
- 简化阴影效果:减少阴影的模糊半径、调整阴影颜色以降低alpha值、避免在频繁变动的视图上使用阴影,或者使用模拟阴影的视觉技巧(如渐变背景)替代。
- 调整透明度与混合模式:尽量避免不必要的透明度设置,尤其是对于大面积或层级较深的视图。对于混合模式,评估是否可以使用视觉效果相近但性能更好的模式,或者避免在性能敏感区域使用非默认混合模式。
- 使用硬件加速功能:如shouldRasterize属性可以让图层内容预先渲染为位图,减少后续绘制时的计算量。但要注意过度使用可能导致内存增加,需权衡利弊。
- 布局与层级优化:减少不必要的视图层级和重叠部分,避免深度过大的视图结构。这有助于减少离屏渲染的需求并提高渲染效率。
- 长列表优化:对于滚动列表,使用UICollectionView或UITableView,并结合cell prefetching、estimatedItemSize、dequeueReusableCell等技术减少不必要的视图创建和销毁,降低离屏渲染的概率。
- 适时使用异步绘制:对于复杂的绘制任务,特别是在主线程中可能导致卡顿或掉帧的情况下,考虑使用异步绘制技术。虽然UIKit框架本身并不直接支持异步绘制UIView,但你可以利用Core Graphics(Quartz 2D)或Metal等更低级别的图形API来在后台线程上执行绘制操作,并将结果作为图像(UIImage)或图层内容(CALayer的contents)在主线程上更新。这样做可以显著减少主线程的负载,提高应用的响应性和流畅度。
- 缓存绘制结果:对于不会频繁变化的复杂视图或图形,可以将其绘制结果缓存为位图(UIImage或CGBitmapContext)。这样,每次需要显示时,只需从缓存中取出位图进行展示,而无需重新进行复杂的绘制操作。缓存技术可以大幅度减少渲染时间,提高性能。但需要注意的是,缓存也会占用额外的内存资源,因此需要合理管理缓存的大小和生命周期。
- 优化图像资源:图像资源是应用中最常见的渲染对象之一。优化图像资源可以显著减少渲染时间和内存占用。这包括使用合适的图像格式(如PNG、JPEG等),根据设备屏幕分辨率提供不同尺寸的图像,以及使用图像压缩算法减少图像文件的大小。此外,对于需要动态修改的图像(如添加滤镜、圆角等),考虑在图像加载时就进行处理,并将处理结果缓存起来,以避免在每次显示时都进行重复的计算和渲染。
- 利用iOS 13及以上版本的Metal Performance Shaders(MPS):如果你的应用需要处理大量的图形数据或执行复杂的图形计算,可以考虑使用Metal Performance Shaders(MPS)来加速渲染过程。MPS是Apple为Metal框架提供的一组高性能计算内核,专门用于图像处理和图形计算任务。通过使用MPS,你可以将复杂的图形算法以更高效的方式实现,并充分利用GPU的并行计算能力来加速渲染速度。
- 分析和测试:在进行离屏渲染优化时,不要忘记使用Xcode提供的工具(如Instruments的Core Animation模板)来分析和测试你的应用性能。这些工具可以帮助你识别哪些视图或图层触发了离屏渲染,并测量渲染所需的时间和资源消耗。通过分析测试结果,你可以更准确地定位性能瓶颈,并采取相应的优化措施。
- 代码审查和重构:定期进行代码审查和重构也是提高渲染性能的重要手段。通过审查代码,你可以发现并消除不必要的离屏渲染操作,如不必要的阴影、透明度设置或混合模式等。同时,通过重构代码,你可以优化视图层级结构、减少视图数量、合并相似的绘制逻辑等,从而进一步提高渲染效率。