携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第27天,点击查看活动详情
本文从苹果在底层是如何渲染画面到屏幕上的过程开始,以此来进一步分析屏幕卡顿的原理,最后进行屏幕卡顿的解决。
主要内容:
- 卡顿原理
- 卡顿优化
- 离屏渲染
1. 卡顿原理
1.1 CRT显示器原理
电子枪从上到下一行一行的扫描,扫描完成后就是一帧画面,之后就回到初始位置进行下一轮扫描,这样就实现了画面显示。 显示器每次扫描需要获取显示的数据,这样就需要与系统的视频控制器进行同步,显示器会用硬件时钟产生一系列的定时信号来进行同步。
定时信号有两种,一个是水平同步信号(HSync),一个是垂直同步信号(VSync)。
绘制过程
- 准备扫描每一行都会发出一个HSync信号
- VSync信号到来后,主线程开始在CPU中计算内容,包括创建视图、布局计算,图片解码、文本绘制等,计算好的内容提交到GPU
- GPU进行变换、合成、渲染,将渲染结果放到帧缓冲区(frameBuffer)
- 视频控制器会在等到下一次VSync信号到来后逐行读取帧缓冲区的数据
- 读取完成后,进行一定的数模转换传递给显示器显示
- 简单来说就是,经过CPU的计算以及GPU的渲染之后,会将帧数据存放到帧缓存区中,之后视频控制器读取到帧缓存区的数据,经过数模转换传递给显示器显示
- 当一帧画面完成,准备画下一帧时,会发出一个VSync
过程示意图:
注意:
- 显示器通常以固定的频率进行刷新画面,也就是每帧画面完成的时间,这个刷新率就是垂直同步信号(VSync)
1.2 画面显示原理
主要理解垂直同步机制
如果只有一个帧缓冲区,帧缓冲区的读取和刷新都有较大的效率问题。因此通常需要引入双缓冲机制,也就是两个帧缓冲区,GPU会预先渲染好一帧放入到帧缓冲区,视频控制区进行读取,在读取的过程中,就可以将新渲染好的一帧放到另一个帧缓冲区,这样就可以一直不停的进行刷新帧缓冲区,而当视频控制器读取完成,GPU会主动的把指针指向第二个缓冲区,这样读取和刷新帧缓冲区的效率都提高了。
但上面的双缓冲机制有一个很大的问题,就是GPU会一直不停的将渲染好的一帧数据放到帧缓冲区中,并且在提交完成后,会主动的把指针指向第二个缓冲区,这样如果此时视频控制器还未读取完成,比如读取到一半,下一半就变成了下一帧的数据,就会造成画面撕裂现象。
正因为帧缓冲区的帧数据混用造成画面撕裂问题,需要引入垂直同步机制。也就是说,之前显示器只是把垂直水平信号同步到了视频控制器,垂直同步机制直接同步到了GPU
理解: GPU会等待显示器发出垂直水平信号(VSync)后,才进行新的一帧渲染和缓冲区更新
本质: 帧缓冲区的更新和读取时同时进行,而且都收到VSync信号的控制,读取上一个帧数据时,更新下一个帧数据
1.3 卡顿(掉帧)原理
在垂直同步机制下就会出现卡顿现象。
上面所说的垂直同步机制,需要VSync到来时,更新帧数据,下一个VSync到来时,会读取这次更新的帧数据。而如果下一个VSync到来时,因为CPU或GPU的原因,帧数据还没有更新到帧缓冲区,就会继续读取上一个帧数据,在一个VSync时间内显示了两次帧数据,就会造成卡顿现象。
CPU和GPU都会阻碍显示流程,都会造成掉帧现象
2. 卡顿优化
分为两类,CPU和GPU。
2.1 CPU资源消耗原因和解决方案
基本原则有两个:第一是避免使用不必要的操作,第二是必需的操作尽量放到后台执行。
避免不必要的操作:
- 对象创建
- 原因:对象的创建会分配内存、设置属性等操作比较消耗CPU资源,如果是反射机制就消耗的更多
- 解决:
- 使用轻量级的对象代替重量级的对象
- 推迟对象创建的时间,并把多个对象的创建分散到多个任务中,不要集中创建
- 如果对象可以复用,可以放到一个缓存池中复用
- 也就是说,如果用功能较少的对象可以完成任务,就不要用功能很多的对象了
- 例如:CALayer比UIVIew要轻量许多,如果不涉及事件响应,只需要显示,就可以用CALayer来代替UIView
- 纯代码编码
- 原因:storyboard创建视图对象消耗资源会比直接使用代码创建对象要大的非常多
- 解决:可以考虑使用纯代码编码
- 对象调整
- 原因:设置属性会消耗比较多的资源,所以需要避免频繁的设置属性
- 解决:尽量避免减少不必要的属性修改
- 例如:尤其是给UIView重设 属性时,比如frame时,其实都是设置了CALayer的属性,而CALayer是没有属性的,是通过方法动态解析实现的,临时为对象创建的一个属性,这非常消耗资源,所以应减少给UIView重置大小等操作
- 尽量避免调整视图层次,添加和移除视图,UIView、CALayer之间会出现很多方法调用与通知
- Autolayout
- 原因:对于复杂视图会产生严重的性能问题
- 解决:可以手动调整属性
必需的操作放到后台执行
- 对象销毁
- 原因:如果大量的对象进行销毁,也会占用一定的资源
- 解决:可以放到后台去释放对象
- 例如:在一个集合中保存有大量的对象,销毁这个集合时就会同时销毁这些对象,尽量放到后台去执行
- 布局计算
- 原因:视图布局的计算是最为常见的消耗CPU资源的地方
- 解决:可以在后台提前计算好视图布局,并进行缓存
- 例如:UIView的属性设置,尽量在后台计算好,一次性调整好属性
- 文本宽高计算
- 原因:文本的宽高计算会占用很大一部分资源,并且不可避免
- 解决:使用[NSAttributedString boundingRectWithSize:options:context:] 来计算文本宽高,用 -[NSAttributedString drawWithRect:options:context:] 来绘制文本,并且这两个方法需要放入到后台线程执行
- 也可以高度缓存
- 文本渲染
- 原因:所有的文本内容控件在底层都是通过CoreText实现的,而它的排版和绘制都是在主线程中,所以当显示大量文本的时候,CPU的压力会非常大
- 解决:在底层的CoreText对文本进行异步绘制
- 图片解码
- 原因:创建图片时,图片的数据并不会立即解码,而是在提交到GPU前CGImage中的数据才会得到解码,而且是在主线程中执行,所以会产生较多的消耗
- 解决:常见的做法是,先在后台把图片绘制到CGBITmapContext中,然后从bitmap直接创建图片
2.2 GPU资源消耗原因和解决方案
GPU:GPU是用图像处理器,它处理的内容都在显存中
消耗资源原因和解决方案
- 纹理的渲染
- 问题1:如果短时间内显示大量的图片,不管是提交到显存的过程,还是GPU进行渲染都要消耗不少GPU资源
- 解决1:尽量避免短时间内显示大量的图片,尽可能的将多张图片合成为一张进行显示
- 问题2:当图片过大,超过GPU的最大纹理尺寸,CPU会进行预处理,会对CPU和GPU带来额外的资源消耗
- 解决2:图片和视图大小不要超过纹理尺寸上限(4096*4096)
- 视图的混合
- 问题:多个视图叠在一起显示,GPU会首先把他们混合到一起再渲染,如果视图结构太过复杂,在混合的过程中也就会消耗资源了
- 解决:减少视图数量和层级
- 图像绘制
- 问题:CALayer的border、圆角、阴影、遮罩(mask)等显示通常会触发离屏渲染,GPU在进行离屏渲染就会消耗一定的资源
- 解决:
- 尽量使用已经设置好的图片,不用GPU去进行设置。
- 把需要显示的图形在后台线程回执未图片,避免使用圆角、阴影、遮罩等属性
3. 离屏渲染
离屏渲染是值GPU在当前屏幕缓冲区之外再开辟一个新的离屏缓冲区,进行渲染。
因为当我们渲染完图层,要进行一些操作的时候,比如设置圆角、阴影、高斯模糊等,之前存放在帧缓冲区的帧数据已经不在了,所以就需要开辟一个离屏缓冲区来存放这些中间状态的数据。
当所有的图层都渲染到离屏缓存区之后,在分别从各个离屏缓冲区取出数据,做出设置圆角等操作后,组合起来存入到帧缓冲区。
利弊:
离屏渲染的代价
- 开辟新的离屏缓冲区会占用空间,会消耗资源
- 切换上下文环境也消耗很多资源
- 离屏渲染明显会等到所有的图层的操作设置好之后再喝到一起存入帧缓冲区,时间会更久,容易出现卡顿现象
好处:
- 对于多次出现的数据,可以提前渲染好,达到复用的目的
- 特殊效果,需要使用离屏缓存区保存中间状态