在开始理解卡顿, 掉帧及绘制原理前, 首先让我们先了解下图像的显示原理
图像显示原理
关于 CPU 和 GPU 都是通过总线连接起来的, 在 CPU 当中输出的往往是一个位图, 再经由总线在合适的时机传递个 GPU
GPU 拿到这个位图之后, 会对这个位图的图层进行渲染, 包括纹理的合成等
之后会把这个结果放到帧缓冲区中, 然后视频控制器会按照 VSync 信号逐行读取帧缓冲区的数据, 经过可能的数模转换传递给显示器, 达到最终的显示效果
那么接下来让我们看一下 CPU 和 GPU 分别做了哪些事情
首先当我们创建一个 UIView 控件的时候, 其中负责显示的 CALayer
CALayer 中有一个 contents 属性, 就是我们最终要绘制到屏幕上的一个位图, 比如说我们创建了一个 UILabel, 那么在 contents 里面就放了一个关于 Hello world 的文字位图
然后系统会在一个合适的时机回调给我们一个 drawRect: 的方法, 这个方法中我们可以去绘制一些自定义的内容
绘制好了之后, 最终会由 Core Animation 这个框架提交给 GPU 部分的 OpenGL 渲染管线, 进行最终的位图的渲染, 包括纹理合成等, 然后显示在屏幕上
那么 CPU 和 GPU 具体做了哪些工作承担呢
CPU
具体分为四个阶段
Layout: 这里主要涉及到一些 UI 布局, 文本计算等, 例如一个 label 的 size
Display: 绘制阶段, 例如 drawRect 方法就在这一步骤中
Prepare: 图片的编解码等操作在此步骤中
Commit: 提交位图
GPU 渲染管线
顶点着色
图元装配
光栅化
片段着色
片段处理
UI 卡顿, 掉帧的原因
在显示器中是固定的频率, 比如 iOS 中是每秒 60 帧(60FPS), 即每帧 16.7ms
从上图中可以看出, 每两个 VSync 信号之间有时间间隔(16.7ms), 在这个时间内, CPU 主线程计算布局, 解码图片, 创建视图, 绘制文本, 计算完成后将内容交给 GPU,GPU 变换, 合成, 渲染(详细可学习 OpenGL 相关课程), 放入帧缓冲区
假如 16.7ms 内, CPU 和 GPU 没有来得及生产出一帧缓冲, 那么这一帧会被丢弃, 显示器就会保持不变, 继续显示上一帧内容, 这就将导致导致画面卡顿
所以无论 CPU,GPU, 哪个消耗时间过长, 都会导致在 16.7ms 内无法生成一帧缓存
卡顿, 掉帧优化方案切入点
CPU CPU 在准备下一帧的所做的工作非常多导致耗时, 基于减轻 CPU 工作时长和压力来达到一个优化效果 1, 部分对象的创建, 调整和销毁可以放到子线程去做 2, 预排版( 布局计算, 文本计算), 这些计算也可以放到子线程去做, 这样主线程也可以有更多的时间去响应用户的交互 3, 预渲染(文本等异步绘制, 图片编解码等)
GPU 1, 纹理渲染: 假如说我们触发了离屏渲染, 例如我们设置圆角时对 maskToBounds 的设置, 包括一些阴影, 蒙层等都会触发 GPU 层面的离屏渲染, 对于这种情况下, GPU 对于纹理渲染的工作量就会非常的大, 我们可以基于此对 GPU 进行优化, 就是尽量减少离屏渲染, 我们也可以通过 CPU 的异步绘制来减轻 GPU 的压力
2, 视图混合: 比如说我们视图层级比较复杂, 视图之间层层叠加, 那么 GPU 就要做每一个视图的合成, 合成每一个像素点的像素值, 如果我们可以减少视图的层级, 也是可以减轻 GPU 的压力, 我们也可以通过 CPU 的异步绘制机制来达到一个提交的位图本身就是一个层级比较少的位图
UIView 的绘制原理
流程图
当我们调用 [UIView setNeedsDisplay] 这个方法时, 其实并没有立即进行绘制工作, 系统会立刻调用 CALayer 的同名方法, 并且会在当前 layer 上打上一个标记, 然后会在当前 runloop 将要结束的时候调用 [CALayer display] 这个方法, 然后进入我们视图的真正绘制过程
而在 [CALayer display] 这个方法的内部实现中会判断这个 layer 的 delegate 是否响应 displayLayer: 这个方法, 如果不响应这个方法, 就会进入到系统绘制流程中; 如果响应这个方法, 那么就会为我们提供异步绘制的入口
上面就是 UIView 的绘制原理, 接下来我们看一下系统绘制流程是怎样的
老规矩, 先上流程图
在 CALayer 内部会先创建 backing store, 我可以理解为 CGContext, 我们一般在 drawRect: 方法中通过上下文堆栈当中取出栈顶的 context, 也就是上下文
然后这个 layer 会判断是否有代理, 如果没有代理, 那么就会调用 [CALayer drawInCotext:]; 如果有代理, 会调用代理的 drawLayer:inContext: 方法, 然后做当前视图的绘制工作(这一步是发生在系统内部的), 然后在一个合适的时机给与我们这个十分熟悉的[UIView drawRect:] 方法的回调,[UIView drawRect:]这个方法默认是什么都不做,, 系统给我们开这个口子是为了让我们可以再做一些其他的绘制工作
然后无论是哪个分支, 最终都会由 CALayer 上传对应的 backing store(可以理解为位图)给 GPU, 然后就结束了系统默认的绘制流程
那么问题来了, 我们如何进行异步绘制呢
实际上我们就需要借用系统给开的这个口子, 即[layer.delegate displayLayer:]
在这个异步绘制过程中就需要代理负责生成对应的 bitmap(位图)
同时设置 bitmap 作为 layer.contents 属性的值
国际惯例, 流程图走一波(原谅我画图能力实在有限 TT)
假如说我们在某一个时机调用了 [view setNeedsDisplay] 这个方法, 系统会在当前 runloop 将要结束的时候调用 [CALyer display] 方法, 然后如果我们这个 layer 的代理实现了 [view displayLayer] 这个方法
然后会通过子线程的切换, 我们在子线程中去做一个位图的绘制, 主线程可以去做一些其他的操作
在子线程中第一步先通过 CGBitmapContextCreate()方法来创建一个位图的上下文, 然后我们通过 CoreGraphic API 可以做当前 UI 控件的一些绘制工作, 最后我们再通过 CGBitmapContextCreateImage()这个函数来根据当前所绘制的上下文来生成一张 CGImage 图片
最后回到主线程来提交这个位图, 设置 layer 的 contents 属性, 这样就完成了一个 UI 控件的异步绘制过程
离屏渲染 (便于理解视图卡顿, 掉帧中对 GPU 的开销)
离屏渲染指的是 GPU 在当前屏幕缓冲区以外开辟了一个缓冲区进行渲染操作
当前屏幕渲染不需要额外创建新的缓存, 也不需要开启新的上下文, 相对于离屏渲染性能更好. 但是受当前屏幕渲染的局限因素限制(只有自身上下文, 屏幕缓存有限等), 当前屏幕渲染有些情况下的渲染解决不了的, 就使用到离屏渲染
离屏渲染对性能的的代价是很高的, 主要体现在:
创建了新的缓冲区
上下文的频繁切换
导致产生离屏渲染的原因:
- shouldRasterize(光栅化)
- masks(遮罩)
- shadows(阴影)
- edge antialiasing(抗锯齿)
- group opacity(不透明)
复杂形状设置圆角等
渐变
来源: https://juejin.im/post/5c0931d451882531b81b20fa