iOS 屏幕渲染得懂点儿

366 阅读7分钟

CPU 和 GPU

屏幕渲染靠的是CPU和GPU的合作才能完成

  • CPU:控制核心,负责逻辑性问题和解决运算问题
  • GPU:绘图计算用的微处理器

我们常用到的GPU功能包括但不限于以下部分:

  • 1、人脸识别
  • 2、AVFoundation的硬件加速器
  • 3、渲染屏幕视图
  • 4、音视频开发中的编码解码
  • 5、metal渲染

显示的发展过程

随机扫描显示

早期的矢量显示器,就是随机扫描显示,通过像素点做简单路径位移绘制出简单图形。

光栅扫描显示

光栅扫描通过图像的像素阵列组成显示,不是直接绘制画面,需要先创建帧缓冲区,里面存储的是下一帧画面。

帧缓冲区

分为颜色缓冲区和深度缓冲区。

颜色缓冲区的每个像素点存储的是颜色值,当图层重叠时会判断最上层展示的图层,只存储展示的颜色值。当图层存在透明度时,会混合颜色后存储。

深度缓冲区的每个像素点存储的是图层深度。

屏幕撕裂与掉帧

屏幕撕裂是什么?

一张图片的上半部分和下半部分存在断层现象 iShot2022-01-12 02.11.08.png

屏幕撕裂形成的原因

首先了解一下单一缓存模式,单一缓存模式就是帧缓冲区只有一个存储空间。

图片是边处理数据边展示的,展示需要经过 CPU -> 内存 -> GPU -> 展示 的过程。

CPU和GPU的协作过程中出现了偏差,GPU应该完整的绘制图片,但是工作慢了只绘制出图片上半部分。

此时CPU又把新数据存储到缓冲区,GPU继续绘制的时候下半部分就变成了新数据。

造成了两帧同时出现了一部分在屏幕上,看起来就撕裂了。

怎么解决撕裂?

解决上一帧和下一帧的覆盖问题,需要使用不同的缓冲区,通过两个图形缓冲区的交替来解决。

出现速度差的时候,就把下一帧存储在后备缓冲区,绘制完成后再切换帧。

iShot2022-01-12 02.25.15.png

存储的问题解决了,但是怎么知道绘制完成呢?

这个时候垂直同步信号就登场了,当绘制完最后一个像素点就会发出这个信号。

所以屏幕撕裂问题需要通过 双缓冲区 + 垂直同步信号 解决。

从左到右,从上往下一行行绘制,黄色的就是垂直同步符号 iShot2022-01-12 02.23.35.png

掉帧问题

虽然上述方案可以解决屏幕撕裂问题,但是会这样粗暴的处理会产生掉帧的问题。

例如:屏幕正在展示A帧的时候,CPU和GPU会处理B帧。

A帧展示完成该切换展示B帧的时候B帧的数据未准备好。

没办法切换就只能重复展示A帧,感官上就是卡了,这就是掉帧的问题。

iShot2022-01-12 02.26.23.png

解决掉帧

掉帧,根本原因是CPU和GPU渲染计算耗时过长。我们可以这样:

  • 1、降低视图层级
  • 2、提前或减少在渲染期的计算

图形图像渲染流程

apple/openGL/metal 文档描述渲染流程极其相似,其渲染流程是几乎一样的。

  • 1、处理点击事件
  • 2、提交,布局计算
  • 3、CPU把图片解码成位图
  • 4、等待下一个runloop,绘制
  • 5、GPU渲染,着色器着色后存在帧缓冲区
  • 6、屏幕展示

什么是着色器

着色器本质是GPU执行的代码段,用于渲染和绘图。

顶点着色器

顶点着色器处理顶点的代码段,包括但不限于以下处理:

  • 1、iOS系统坐标和屏幕物理坐标系的转换。
  • 2、核心动画,修改图形的位置(平移缩放旋转),顶点与矩阵相乘就会得到顶点旋转之后的位置。
  • 3、手机屏幕是二维的,但是需要展示三维的内容,其实就是模拟三维显示,这需要深度计算。

片元着色器

片元着色器是计算每个像素点的颜色值的代码段,包括但不限于以下处理:

  • 1、显示图片的颜色值。
  • 2、调整图片的饱和度,每个像素点进行饱和度计算

计算后的值会放到帧缓冲区。

着色器渲染流程

  • 1、确定绘制图形的位置,拿到iOS的系统坐标,需要换算成屏幕坐标系。
  • 2、转换后确定好顶点的位置,这时候就需要图元装配,这个就是确定顶点间的连线关系。
  • 3、确定连接方式以后需要进行光栅化,就是把展示用到的像素点摘出来。
  • 4、摘出来以后GPU进行片元着色器处理,计算摘出来的像素点展示的颜色,并存入缓冲区。 iShot2022-01-12 02.30.46.png

离屏渲染

什么是离屏渲染

普通渲染流程:APP - 帧缓冲区 - 展示

离屏渲染流程:APP - 离屏渲染缓冲区 - 帧缓冲区 - 展示

离屏渲染,是无法一次性处理渲染,需要分部处理并存储中间结果引起的。

所以判断是否出现离屏渲染的根本条件就是判断渲染是否需要分部处理~

  • 需要分部处理,会产生离屏渲染

  • 一次性渲染,不产生离屏渲染

离屏渲染的影响

需要分几步就需要开辟出几个离屏渲染缓冲区存储中间结果,造成空间浪费。

最后合并多个离屏渲染缓冲区才能展示结果,会影响性能。

离屏渲染案例

  • 1、使用了 mask.layer
  • 2、裁剪 layer,使用 layer.maskToBounds/ view.clipsToBounds
  • 3、添加投影 layer.shadow
  • 4、绘制文字的 layer
  • 5、主动打开光栅化(shouldRasterize)也会离屏渲染,但是这个情况不一样。

当不大于屏幕2.5倍且不需要修改的图层在短时间内需要被复用,是不应该进行多次渲染的。

此时打开光栅化就可以使上述图层进行一次渲染多次复用,达到提高性能的效果。

光栅化具体条件限制:

  • 1、如果layer不复用,不打开
  • 2、layer不是静态的,会被频繁修改,不打开
  • 3、100ms内容没有被使用会被丢弃,不打开
  • 4、缓存空间有限,超过屏幕2.5倍大小的图像会失效,无法复用

圆角引起的离屏渲染

先了解一个概念,画家算法:根据深度值确定绘制顺序。先绘制较远的图层,再绘制较近的图层。

当控件具有多个图层,添加圆角的时候不可以一次性剪切所有图层,只能绘制一层剪切一层。

没添加圆角不产生离屏渲染的时候:

  • 绘制layer1存在帧缓冲区然后展示到屏幕,清空帧缓冲区。
  • 绘制layer2存在帧缓冲区然后展示到屏幕,清空帧缓冲区。
  • 绘制layer3存在帧缓冲区然后展示到屏幕,清空帧缓冲区。 此时一步一步并不需要用到离屏缓冲区。

添加圆角的时候: 帧缓冲区的图片需要处理圆角,就不能放在帧缓冲区只能放在离屏缓冲区。

  • 创建离屏缓冲区1存放layer1。
  • 创建离屏缓冲区2存放layer2。
  • 创建离屏缓冲区3存放layer3。
  • 每个离屏缓冲区取出数据的时候剪切圆角。
  • 最后一次性合并到帧缓冲区,展示。

给出4个常见解决方案:

  • 1、直接替换成圆角资源。
  • 2、在最上层盖上一个圆形中空的mask盖住四角。
  • 3、使用贝塞尔曲线,上下文设置只有内部可见,把原来的图像绘制进去,这种方式需要监听frame、color等属性触发重绘。
  • 4、重写drawRect手动绘制,但是多次调用会有性能问题。