iOS视图控件的内容显示和离屏渲染流程
阅读原文时间:2023年08月30日阅读:2

iOS中UI控件内容显示流程

UIKit界面组成

iOS中组成页面的各个元素基本来自UIKit,我们可以修改布局或自定义绘制来修改UIKit元素的默认展示。

UIView的页面显示内容有CALayer负责,事件的接收与响应由UIView自己负责。

为什么需要有这样的分工呢,原因是因为Mac上和iPhone上的事件存在很大的区别,iPhone 是屏幕触摸事件,Mac上是鼠标,键盘等事件,但是显示上却是高度一致的,因此把显示部分单独封装成CALayer而存在来。

UIView默认是CALayer的CALayerDelegate,它负责创建并管理它的图层,以确保当子视图在层级关系中添加或者被移除的时候,它们关联的图层也同样对应在层级关系树当中有相同的操作。

每个View被创建的时候都会自动创建一个CALayer,同时还可以在后续的操作中添加多个layer。

CALayer有个id类型的contents属性,它指向内存中的一个成为backing storage的存储空间。往contents上赋值的时候就会将UIView的显示内容存储到这个backing storage中,

这里个id类型是一个兼容的写法,它在iOS上时CGImageRef类型,在Mac OS上是NSImage类型。

如何将显示的内容绘制到CALayer上

在创建流程中可分成两大分支:

1.通过CALayer的Delegate绘制

简单理解为通过实现UIView的代理方法displayLayer:或者重写CALayer的display方法,手动给layer.contents赋值,将内容绘制到CALayer 默认的backing store上。

在我们调用[UIView setNeedsDisplay]的时候,会触发[view.layer setNeedsDisplay],紧接着调用[view.layer display] 在这个方法中会判断layer.delegate 是否实现了displaylayer如果有则将layer传递出去,

然后在UIView的displayLayer:(CALayer *)layer方法中对contents进行赋值。注意:UIView默认为layer.delegate。

具体案例有:SDAnimatedImageView的代理实现

- (void)displayLayer:(CALayer *)layer {
UIImage *currentFrame = self.currentFrame;
if (currentFrame) {
layer.contentsScale = currentFrame.scale;
layer.contents = (__bridge id)currentFrame.CGImage;
}
}

另一种实现方法是在CALayer中复写layer的display方法,在其中对contents进行赋值

具体案例有:YYTextAsyncLayer的重写实现

- (void)display {
super.contents = super.contents;
[self _displayAsync:_displaysAsynchronously];
}

2.使用系统内部绘制

系统开始的时候会创建一个新的backing store,然后开始走drawInContext,这时候会先看layer.delegate是否实现了drawRect

如果有则用drawRect,

否则调用drawLayer:inContext:

并将管理新建backing store的context传递出来。

提交图层树到Render Server

UIView的显示内容创建好之后,后面就是准备渲染了。

在一个界面从开始到提交到Render Server前一共可以分成三个步骤:

Layout
Prepare && Display
Commit

Layout

一个控件在添加到界面上时,会自动触发布局,从而确定整个层级数中每个控件的frame。

Prepare && Display

这部分会涉及到图片的解码,文本绘制,或者通过CALayer暴露出来的CGContextRef在backing store中进行绘制。

图片解码一般发生在Prepare阶段。

存储在backing store的 bitmap后续就会被打包送到Render Server中。

Commit

当RunLoop即将进入休眠期间或者即将退出的时候,会通过已经注册的通知回调执行_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv函数,

在这个函数会递归将待处理的图层进行打包压缩,并通过IPC方式发送到Render Server。

这时候的Core Animation会创建一个OpenGL ES纹理并将backing store中的位图上传到对应的纹理中。

在将图层树发送到GPU之前Core Animation做的处理工作

Render Server在拿到压缩后的数据的时候,首先对这些数据进行解压,从而拿到图层树,然后根据图层树的层次结构,每个层的alpha值opeue值,RGBA值、以及图层的frame值等对被遮挡的图层进行过滤,删除无需渲染的图层,最终得到渲染树,渲染树就是指将图层树对应每个图层的信息,比如顶点坐标、顶点颜色这些信息,抽离出来,形成的树状结构。GPU收到的原始处理数据就是这课渲染树。

然后将渲染树发送到GPU,GPU开启真正的渲染流程。

GPU的渲染流程

往细得分可以分成六个阶段

顶点着色器(Vertex Shader):

在Render Server 拿到顶点数据并输入到渲染管线的时候,顶点着色器会对每个顶点数据进行一次运算,每个顶点都对应一组顶点数组,这些数组可以用于存储:顶点坐标,RGBA颜色,辅助颜色,纹理坐标以及多边形边界标志等。

图元装配(Shape Assembly):

图元装配的过程就是将顶点连接起来,形成一个个所支持的图元元素

几何着色器(Geometry Shader):

把图元装配后的产物,图元形式的一系列顶点的集合作为输入,来产生新顶点构造出新的图元来生成其他形状

光栅化(Rasterization):

光栅化会把图元映射为最终屏幕上相应的像素,生成供片段着色器使用的片段,OpenGL中的一个片段是OpenGL渲染一个像素所需的所有数据,它包含位置,颜色,纹理坐标等信息。

裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。

片段着色器(Fragment Shader):

片段着色器的主要目的是计算一个像素的最终颜色,包括光照、阴影、光的颜色等等,这些数据可以被用来计算最终像素的颜色。

根据顶点着色器输出的顶点纹理坐标对纹理贴图进行采样,以计算该片段的颜色值。从而调整成各种各样不同的效果图。

测试与混合(Tests and Blending):

检测片段的对应的深度值,用它们来判断这个像素是其它物体的前面还是后面,决定是否应该丢弃。这个阶段也会检查alpha值并对物体进行混合(Blend)。

屏幕显示器显示原理

显示器和GPU的关系是生产消费者关系,GPU生成要显示的图像数据放到帧缓冲区,显示器从帧缓冲区读取出来,在屏幕上展示。

显示器的展示原理是使用电子枪从左上角到右下角逐屏扫描的。扫描枪在扫描过程中严格根据扫描枪信号进行扫描。

当水平信号HSync来时,扫描枪从左到右扫描一行,然后移动到下一行等待,当下个HSync到来时,重复上一个操作。

当一屏幕扫描完成,扫描枪回到左上角,等待VSync下一个垂直信号的到来。

显示器和GPU之间使用了双缓存机制,在显示器显示某帧数据的时候,GPU可以往另一个缓存中提交渲染好的数据,在VSync信号到来的时候,视频控制器切换到另一个缓存用于显示,如果在规定时间1/60s内没有完成往另一个缓存中写入要展示的数据,这时候视频控制器就不会将缓存切换到未完成的帧,而是继续显示当前的内容。这就给人们带来视觉上的卡顿。

离屏渲染

屏幕渲染流程有2种方式:正常渲染流程,离屏渲染流程

正常渲染流程

CPU通过布局计算,文本绘制,图片解码,将得到的渲染树通过Core Animation提交给GPU

GPU使用画家算法,根据图层距离屏幕的距离由远到近分别对图层进行纹理映射,顶点着色,光栅化等得到每个图层的像素效果,再通过图层混合,透明计算,深度计算得出可以展示的图像信息放到帧缓存区

视频控制器在每个垂直信号来到时,读取帧缓存区数据在屏幕上展示,然后立即丢弃这帧数据,不做任何保留。这样可以提高渲染性能,不同帧数据各自独立。

离屏渲染流程

当给视图设置圆角,阴影,蒙版这些图层预合成属性时,表示视图内容(包括layer及其所有的sublayers)在其预合成之前是不能在屏幕中绘制的,即:预合成之前不能放到帧缓冲区。

因为帧缓存区的图层数据是用完就丢弃,本地不会记录,所以需要开辟一个离屏缓存区用来存储视图中所有要处理的图层数据,先按画家算法由远到近逐个将图层渲染近缓存区,然后再按画家算法的顺序由远到近的进行画圆角,最后把它们合并叠加,把结果一起放到帧缓存区中。

视频控制器在每个垂直信号来到时,读取帧缓存区数据在屏幕上展示。

触发离屏渲染的方式

1.shouldRasterize(光栅化)
2.masks(遮罩)
3.shadows(阴影)
4.edge antialiasing(抗锯齿)
5.group opacity(不透明)
6.复杂形状设置圆角等
7.渐变

离屏渲染的问题

增加性能消耗,可能导致掉帧,CPU从收到的渲染树到转成bitmap写到帧缓冲区,这一套流水线是源源不断的。而因为要使用离屏渲染,则先要把每个图层的处理结果不断记录在离屏缓冲区,最后还要做进行额外的处理,然后再把处理结果移动到当前帧缓冲区。这是二个流程的切换,中间要记录上下文。这个额外的操作对于

性能消耗比较大,如果不能1/60s完成,可能造成掉帧

离屏渲染要额外开辟一内存进行预合成操作浪费内存。

离屏渲染的优点

保存中间状态:如果一些视图状态不能一次性完成,则可以临时保存中间状态,如圆角,阴影,蒙版。

提升渲染效率:如果一个状态要多次渲染,可以提前渲染完成放到离屏缓冲区,等屏幕展示时直接使用。如开启光栅化

复用离屏渲染结果

shouldRasterize(光栅化)

当设置视图的shouldRasterize = YES,开启光栅化时,系统会将视图离屏渲染得到的结果(如:添加了阴影,遮罩后的结果)保存到位图中缓存起来,这里的位图中的元素和帧缓冲区的像素是一一对应的。

如果视图的 layer 及其 sublayers 都没有发生变化,则在下一帧渲染时直接拿来复用,提供了渲染效率。

光栅化是把GPU的渲染工作从GPU挪到了CPU,并将结果位图做了缓存,等屏幕展示时,直接拿来复用。

这对视图内容复杂,绘制起来麻烦,而又不怎么变化的场景比较适合(它会将整个视图作为一张图片进行保存,等展示时直接拿来复用),而对于经常变化需要重绘的视图如tableViewCell,则返回会增加内存消耗,因为TableViewCell有复用机制,Cell中的内容会经常变化。

光栅化使用建议如下:

1.如果layer不能被复用,则没有必要开启光栅化

2.如果layer不是静态,需要被频繁修改(例如动画过程中),此时开启光栅化反而影响效率

3.离屏渲染缓存内容有时间限制,如果100ms内没有被使用,那么就会丢弃,无法进行复用

4.离屏渲染的缓存空间有限,是屏幕的2.5倍,超过2.5倍屏幕像素大小的话也会失效,无法实现复用

Instruments 监测离屏渲染

1)Color Offscreen-Rendered Yellow,开启后会把那些需要离屏渲染的图层高亮成黄色,这就意味着黄色图层可能存在性能问题。

2)Color Hits Green and Misses Red,如果 shouldRasterize 被设置成YES,对应的渲染结果会被缓存,如果图层是绿色,就表示这些缓存被复用;如果是红色就表示缓存会被重复创建,这就表示该处存在性能问题了。

平衡CPU与GPU

GPU部分:GPU擅长图形处理,这依赖与GPU内部有成千上万的计算单位可以并行运算

CPU部分:

而对于文字(CoreText使用CoreGraphics渲染)和图片(ImageIO)渲染,由于GPU并不擅长做这些工作,不得不先由CPU来处理好以后,再把结果作为texture传给GPU

可以使用CoreGraphics给图片加上圆角,就不需要再另外给图片容器设置cornerRadius了,可以在CPU空闲的时候进行操作

注意:

1.渲染不是CPU的强项,调用CoreGraphics会消耗其相当一部分计算时间,一般来说CPU渲染都在后台线程完成(这也是AsyncDisplayKit的主要思想),然后再回到主线程上,把渲染结果传回CoreAnimation。

2.CPU只适合渲染静态的元素,如文字、图片

3.作为渲染结果的bitmap数据量较大(形式上一般为解码后的UIImage),消耗内存较多,所以应该在使用完及时释放

4.如果使用CPU来做渲染,就没有理由再触发GPU的离屏渲染了

参考文章:

https://zhuanlan.zhihu.com/p/381766140

https://juejin.cn/post/6950920557445513229

https://www.cnblogs.com/mysweetAngleBaby/p/16341632.html

https://tbfungeek.github.io/2019/08/04/iOS-渲染系统工作原理介绍/