主要苹果在底层是如何渲染画面到屏幕上的过程,以此来进一步分析屏幕卡顿的原理,最后进行屏幕卡顿的解决。
主要内容:
- 渲染流程
- 卡顿原理
- 卡顿的解决
1、渲染流程
1.1 整体渲染流程
整体渲染流程.png说明:
大体上分为三部分处理,第一部分是CPU的计算,第二部分是GPU的渲染,第三部分是屏幕显示
- CPU的计算主要是通过核心动画进行处理,之后通过调用OpenGL ES/Metal来传递数据给GPU,编程顶点着色器、片元着色器,还可以设置GPU渲染的具体业务
- GPU渲染主要是将接收到的绘图数据进行一系列渲染最后将帧数据存储到帧缓存区中,供视频控制器调用
- 屏幕显示中是通过视频控制器从帧缓存区获取到帧数据扫描到屏幕上
1.2 CPU计算
CPU的计算苹果底层主要是通过核心动画来实现,包含两部分,第一是对CALayer的计算,第二是调用OpenGL ES/Metal库进行调用GPU
1.2.1 框架的简单认识
CoreGraphics
- 基于Quartz的高级绘图引擎,提供了非常强大的轻量级2D渲染能力
- 在运行时绘图
- 可以处理基于路径的绘图、转换、颜色处理、离屏渲染、图案、渐变和阴影等
- CG打头的类都是属于CoreGraphics Framework
UIKit:
- 最常用的视图框架,通过设置UIKit组件的布局和相关属性来搭建界面
- UIKit负责对用户事件的响应、界面展示、UIVIew就属于UIKit
- UIKit自身并不具备控件显示的能力,是通过下层CoreAnimation框架的CALayer来实现的
CoreAnimation:
在苹果官方的描述中,Render、Compose,and animate visual elements,因此不能被名字欺骗了,其实CoreAnimationg中的动画只是一部分,它其实是一个复合引擎,主要的职责包括 渲染、构建和动画实现。
我们平常用的CALayer来自于CoreAnimation框架,CALayer是屏幕上用户可见内容的基础,主要是由于可视化内容到最后都会被分解成独立的图层(layer),被存储在图层树中。
核心动画所处的位置
说明:
- 上层的UIKit是基于CoreAnimation的,我们知道UIView属于UIKit框架的,CALayer属于CoreAnimation,因此UIView其实是基于CALayer显示的。
- UIView主要负责界面布局和子视图的管理,还可以处理用户的点击事件
- CALayer只负责视图的显示
- CoreAnimation进行视图的显示在底层是通过Metal和CoreGraphics来实现的
- 视图的渲染是通过Metal来实现的
- CoreGraphics主要绘图系统,常用于绘制自定义视图
- CoreAnimation作为中间层向下得到自己的视图,向上提供视图展示给UIKIT来使用
- 最后是调用硬件绘图
CoreImage
- 拥有一系列线程的图像过滤器,可以对已经存在的图像进行高效处理
- 运行前绘制图像,与CoreGraphics正好 相反
- 大部分情况,会在GPU中完成工作,如果GPU很忙,会使用CPU处理
OpenGL ES/Metal
渲染API,详情可以查看博客音视频开发:OpenGL + OpenGL ES + Metal 系列文章汇总
2018年之后苹果底层已经从OpenGL ES切换到Metal渲染了。
1.2.2 CPU计算详细过程
CPU计算详细过程.png说明:
- UIKit的UIView可以获取到事件的处理,对于视图的展示会交给CoreAnimation来处理,
- 而CoreAnimation的处理就是进行内容计算,对计算出的图层进行打包,在下一次runloop中提交到渲染服务
- 渲染服务拿到包后会进行解码,并调用OpenGL ES/Metal来进行渲染。当然具体的渲染工作是GPU来执行的。
扩展:
UIView和CALayer的区别:
UIView
- UIView属于UIKIt框架
- 负责绘制图形和动画操作
- 用于界面布局和子视图的管理
- 处理用户的点击事件
CALayer:
- CALayer属于CoreAnimation框架
- 只负责显示,且显示的是位图
二者关系:
- UIView属于UIKit框架,可以处理用户触摸事件,用于界面布局并管理子视图
- CALayer属于CoreAnimation框架,而CoreAnimation是基于QuartzCode的。所以CALayer只负责显示,不能处理用户的触摸事件
- 从父类来说,CALayer继承的是NSObject,而UIView是直接继承自UIResponder的,所以UIVIew相比CALayer而言,只是多了事件处理功能
- 因此在应用层面来说,需要与用户交互时,使用UIView,不需要交互时,使用两者都可以
1.3 GPU渲染
GPU的渲染流程不再追溯,当前使用Metal来进行渲染,因此Metal的渲染流程可以看十三、Metal - 初探。这里进行简单说明
说明:
- 我们将顶点缓存区中的顶点坐标数组、颜色数组、纹理坐标、变换矩阵等传递到顶点着色器中,顶点着色器会对顶点进行处理,如果涉及到颜色数据会传递给片元着色器供片元着色器来处理
- 顶点着色器处理完成后会进行图元装配,将顶点装配成图元
- 在光栅化时将图元转化为二维片段,可以看做是没有色值的像素点
- 之后将这个二维片段让片元着色器主要进行一下上色处理,就变为了一个像素点。
- 接下来就是将帧数据放到帧缓存区中
1.4 屏幕显示
屏幕显示的操作是从帧缓存区中拿到帧数据,并且显示到显示屏上
1.4.1 CRT显示器原理
屏幕扫描.png说明:
- 电子枪从上到下一行一行的扫描,扫描完成后就是一帧画面,之后就回到初始位置进行下一轮扫描,这样就实现了画面显示
- 显示器每次扫描需要获取显示的数据,这样就需要与系统的视频控制器进行同步,显示器会用硬件时钟产生一系列的定时信号来进行同步
- 定时信号有两种,一个是水平同步信号(HSync),一个是垂直同步信号(VSync)
- 准备扫描每一行都会发出一个HSync信号
- 当一帧画面完成,准备画下一帧时,会发出一个VSync
- 显示器通常以固定的频率进行刷新画面,也就是每帧画面完成的时间,这个刷新率就是垂直同步信号(VSync)
1.4.2 画面显示过程
屏幕显示过程.png说明:
- VSync信号到来后,主线程开始在CPU中计算内容,包括创建视图、布局计算,图片解码、文本绘制等,计算好的内容提交到GPU。
- GPU进行变换、合成、渲染,将渲染结果放到帧缓冲区(frameBuffer)
- 视频控制器会在等到下一次VSync信号到来后逐行读取帧缓冲区的数据
- 读取完成后,进行一定的数模转换传递给显示器显示
- 简单来说就是,经过CPU的计算以及GPU的渲染之后,会将帧数据存放到帧缓存区中,之后视频控制器读取到帧缓存区的数据,经过数模转换传递给显示器显示
1.4.3 画面显示原理
1.4.3.1 读取效率低
通过引入双缓存机制来解决读取效率低的问题
如果只有一个帧缓冲区,帧缓冲区的读取和刷新都有较大的效率问题。因此必须等上一帧读取完之后才能才能将下一帧写入到缓存区中。效率较低。
因此苹果引入双缓冲机制,也就是两个帧缓冲区,GPU会预先渲染好一帧放入到帧缓冲区,视频控制区进行读取,在读取的过程中,就可以将新渲染好的一帧放到另一个帧缓冲区,这样就可以一直不停的进行刷新帧缓冲区,而当视频控制器读取完成,GPU会主动的把指针指向第二个缓冲区,这样读取和刷新帧缓冲区的效率都提高了
双缓存区.png1.4.3.2 画面撕裂问题
通过垂直同步机制来较大程度解决画面撕裂问题
上面的双缓冲机制有一个很大的问题,就是GPU会一直不停的将渲染好的一帧数据放到帧缓冲区中,并且在提交完成后,会主动的把指针指向第二个缓冲区,这样如果此时视频控制器还未读取完成,比如读取到一半,下一半就变成了下一帧的数据,就会造成画面撕裂现象
画面撕裂.png
解决:
VSync信号到来后,才开始CPU->GPU->缓冲区,而此时视频控制器会把上一次的帧数据读取到,可以说是读取和更新是同时的,但是读取和更新都依赖于VSync
帧缓冲区的更新和读取时同时进行,而且都收到VSync信号的控制,读取上一个帧数据时,更新下一个帧数据
比较:
- 第一种做法:单缓冲区,缓冲区会在读取之后再更新
- 第二种做法:双缓冲区,GPU会不停的将帧数据放入到帧缓冲区,读取和更新是同时的,读取依赖于VSync,更新不依赖,会一直更新
- 第三种做法:双缓冲机制+垂直同步机制,VSync信号到来后,才开始CPU->GPU->缓冲区,而此时视频控制器会把上一次的帧数据读取到,可以说是读取和更新是同时的,但是读取和更新都依赖于VSync
2、卡顿原理
上面我们已经知道了视图数据渲染到屏幕上所需要经历的过程,最后视频控制器是按照双缓存机制+垂直同步信号来获取帧数据的。因此我们按照这个认知来分析卡顿原理
上面所说的双缓冲机制+垂直同步机制,需要VSync到来时,更新帧数据,下一个VSync到来时,会读取这次更新的帧数据,而如果下一个VSync到来时,因为CPU或GPU的原因,帧数据还没有更新到帧缓冲区,就会继续读取上一个帧数据,在一个VSync时间内显示了两次帧数据,就会造成卡顿现象
卡顿.png3、卡顿解决
3.1 卡顿监测
- 思路一:监控一秒钟内的帧数是否经常低于或远低于 60FPs。
- 思路二:监控每一帧的时长是否超时。
思路一实现方法:用 CADisplayLinker 来计数
CADisplayLink可以以屏幕刷新的频率调用指定selector,iOS系统中正常的屏幕刷新率为60次/秒,只要在这个方法里面统计每秒这个方法执行的次数,通过次数/时间就可以得出当前屏幕的刷新率了。
思路二实现方法:通过子线程监测主线程的RunLoop,判断两个状态RunLoop的状态区域之间的耗时是否达到一定阈值。
开启子线程,实时计算这两个状态区域之间的耗时是否到达某个阀值,便能揪出这些性能杀手,假定连续6次超时50ms认为卡顿(当然也包含了单次超时300ms)
3.2 卡顿优化
3.2.1 CPU优化
总的来说有两种,第一是避免使用不必要的操作,第二是必需的操作尽量放到后台执行
避免不必要的操作
- 减少一次性对象创建
- 对象的创建会分配内存、设置属性等操作比较消耗CPU资源,如果是反射机制就消耗的更多
- 1、使用轻量级的对象代替重量级的对象
- 2、推迟对象创建的时间,并把多个对象的创建分散到多个任务中,不要集中创建
- 3、如果对象可以复用,可以放到一个缓存池中复用
- 例如:CALayer比UIVIew要轻量许多,如果不涉及事件响应,只需要显示,就可以用CALayer来代替UIView
- storyboard创建视图对象,起消耗资源会比直接使用代码创建对象要大的非常多,可以考虑使用纯代码编码
- 减少属性设置
- 设置属性会消耗比较多的资源,所以需要避免频繁的设置属性
- 尽量避免调整视图层次,添加和移除视图,UIView、CALayer之间会出现很多方法调用与通知
- 减少使用Autolayout
- 对于复杂视图会产生严重的性能问题
- 可以手动调整属性
必要的操作放到后台执行
- 对象销毁
- 如果大量的对象进行销毁,也会占用一定的资源,可以放到后台去释放对象
- 例如:在一个集合中保存有大量的对象,销毁这个集合时就会同时销毁这些对象,尽量放到后台去执行
- 布局计算
- 视图布局的计算是最为常见的消耗CPU资源的地方
- 解决:可以在后台提前计算好视图布局,并进行缓存
- 例如:UIView的属性设置,尽量在后台计算好,一次性调整好属性
- 文本宽高计算
- 文本的宽高计算会占用很大一部分资源,并且不可避免
- 解决:使用[NSAttributedString boundingRectWithSize:options:context:] 来计算文本宽高,用 -[NSAttributedString drawWithRect:options:context:] 来绘制文本,并且这两个方法需要放入到后台线程执行
- 文本渲染
- 所有的文本内容控件在底层都是通过CoreText实现的,而它的排版和绘制都是在主线程中,所以当显示大量文本的时候,CPU的压力会非常大
- 解决:在底层的CoreText对文本进行异步绘制
- 图片解码
- 创建图片时,图片的数据并不会立即解码,而是在提交到GPU前CGImage中的数据才会得到解码,而且是在主线程中执行,所以会产生较多的消耗
- 解决:常见的做法是,先在后台把图片绘制到CGBITmapContext中,然后从bitmap直接创建图片
3.2.2 GPU优化
GPU的认识:
可进⾏绘图运算⼯作的专⽤微处理器,是连接计算机和显示终端的纽带。
他所做的事情概括起来:1、接收提交的纹理(Textture)和顶点描述。2、应用变换(transform)3、混合并渲染 4、输出到屏幕上
纹理的渲染
- 问题1:如果短时间内显示大量的图片,不管是提交到显存的过程,还是GPU进行渲染都要消耗不少GPU资源
- 解决:尽量避免短时间内显示大量的图片,尽可能的将多张图片合成为一张进行显示
- 问题2:当图片过大,超过GPU的最大纹理尺寸,CPU会进行裁剪处理,会对CPU和GPU带来额外的资源消耗
- 解决:图片和视图大小不要超过纹理尺寸上限(4096*4096)
视图的混合
- 多个视图叠在一起显示,GPU会首先把他们混合到一起再渲染,如果视图结构太过复杂,在混合的过程中也就会消耗资源了
- 解决:减少视图数量和层级
图像绘制
- CALayer的border、圆角、阴影、遮罩(mask)等显示通常会触发离屏渲染,GPU在进行离屏渲染就会消耗一定的资源
- 解决:1、尽量使用已经设置好的图片,不用GPU去进行设置。2、把需要显示的图形在后台线程回执未图片,避免使用圆角、阴影、遮罩等属性
网友评论