OC底层原理38:界面优化方案
前言
本文主要讲:
- 卡顿原理:卡顿的原因 -
掉帧
- 卡顿检测工具
- 实战项目优化
界面卡顿
通常来说,计算机中的显示过程是下面这样的,通过 CPU、GPU、显示器
协同工作来将图片显示到屏幕上的
CPU
计算好显示的内容
,提交到GPU
GPU
经过渲染
完成后,将渲染的结果放入FrameBuffer(帧缓存区)
随后
视频控制器
会按照VSync(垂直同步)
信号逐行读取FrameBuffer
的数据经过可能的
数模转换
传递给显示器
进行显示
最开始时,FrameBuffer只有一个,这种情况下 FrameBuffer
的读取和刷新有很大的 效率
问题,为了解决这个问题,引入了 双缓存区
,即 双缓冲机制
,即 前FrameBuffer
和 后FrameBuffer
,GPU渲染结果来回切换放入 前、后FrameBuffer
中。视频控制器来回切换读取 前后FrameBuffer
,交给 显示器显示
双缓存机制
虽然解决了 效率问题
,但是随之而言的是新的问题,当视频控制器还未 读取的频率
和 渲染的频率
不一致时,就会产生 掉帧
,例如屏幕内容显示一半,GPU将新的一帧内容提交到 FrameBuffer,并将两个FrameBuffer而进行交换后,视频控制器就将新的一帧数据的下半段显示到屏幕上,造成 屏幕撕裂
现象
为了解决这个问题,采用了 垂直同步信号机制
,即固定屏幕刷新率 每秒刷新60次
,当开启垂直同步后,GPU会等待显示器的VSync信号发出后,才进行新的一帧渲染和FrameBuffer更新,而目前iOS设备中采用的正是 双缓存区+VSync
屏幕卡顿原因
下面我们来说说,屏幕卡顿的原因
在 VSync
信号到来后,系统图形服务
会通过 CADisplayLink
等机制通知 App
,App 主线程开始在 CPU
中计算显示内容,随后CPU会将计算好的内容提交到GPU去。由GPU进行变换、合成、渲染。随后 GPU
会把渲染结果提交到 帧缓冲区
去,等待下一次 VSync
信号到来时显示到屏幕上。由于垂直同步信号的机制,如果在一个VSync时间内,CPU后者GPU没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示
,而这时显示屏会保留之前的内容不改变。所以可以简单理解 掉帧
为 过时不候
如下图所示,是一个显示过程,第1个在VSync到来期间,GPU渲染提前完成,正常显示;第2个在VSync期间,仍在处理中,此时屏幕不刷新,依旧显示第1帧;第3个VSync期间,切换到另一个FrameBuffer,让CPU渲染;此时就出现了 掉帧2
情况,渲染时就会出现明显的 卡顿现象
从图中可以看出,CPU和GPU不论是哪个阻碍了显示流程,都会造成 掉帧
现象,所以为了给用户提供更好的体验,在开发中,我们需要进行 卡顿检测
以及相应的 优化
卡顿监控
卡顿监控的方案一般有三种:
YYFPSLabel:通过
CADisplayLink + YYWeakProxy
FPS监控
:为了保持流程的UI交互,App的刷新频率应该保持在60fps
左右,其原因是因为iOS
设备默认的刷新频率是60次/秒
,而1次刷新(即VSync信号发出)的间隔是1000ms/60次= 16.67ms/1次
,所以如果在16.67ms
内没有准备好下一帧数据,就会产生卡顿主线程卡顿监控
:通过子线程监测主线程Runloop
,判断两个状态(kCFRunLoopBeforeSource
和kCFRunLoopAfrerWaiting
)之间的耗时是否达到一定阀值
FPS监控
FPS的监控,主要是通过 CADislayLink
实现,借助 link
的时间差,来计算一次刷新所需要的时间,然后通过 刷新次数/时间差
得到刷新频次,并判断是否在其范围,通过显示不同的文字颜色来表示卡顿严重成都。
- 创建一个
SPSDisplay
工具类,实现如下代码
1 | #import <UIKit/UIKit.h> |
1 | #import "FPSDisplay.h" |
- 使用
1 | [FPSDisplay shareFPSDisplay]; |
主线程卡顿监控
除了FPS,还可以通过 RunLoop
来监控,因为卡顿的是事务,而事务是交由 主线程
的 RunLoop
处理的。
也可以直接使用三方库
OC
可以使用 微信matrix 、滴滴DoramonKitSwift
的卡顿检测第三方 ANREye ,其主要思路是:创建子线程进行循环监测,每次检测时设置标记置为true,然后派发任务到主线程,标记置为false,接着子线程睡眠超过阀值时,判断标记是否为false,如果没有,说明主线程发生了卡顿
界面优化
CPU层面的优化
尽量
用轻量级的对象
代替重量级的对象,可以对性能有所优化,例如: 不需要相应触摸
时间的控件,用CALayer
代替UIView
尽量减少 对
UIView
和CALayer
的属性修改CALayer内部并没有属性,当调用属性方法时,其内部是通过运行时
resolveInstanceMethod
为对象临时添加一个方法,并将对应属性值保存在内部的一个 Dictionary中,同时还会通知delegate、创建动画等,非常haoshiUIView
相关的显示属性,例如frame、bounds、transform等,实际上都是从CALayer映射来的,对齐进行调整时,消耗的资源比一般属性要大
当有大量对象释放时,也是非常耗时的,尽量挪到后台线程去释放
尽量
提前计算视图布局
,即预排版
,例如cell的行高Autolayout
在简单页面情况下我们可以很好的提升开发效率,但是对于复杂视图而言,会产生严重的性能问题,随着视图数量的增长,Autolayout
带来的CPU消耗是程指数上升的,所以尽量使用代码布局
,如果不想手动调整frame等,也可以借助三方库,例如Masonry(OC)、SnapKit(swift)
等等文本处理的优化:当一个界面有大量文本时,其行高的计算、绘制也是非常耗时的
如果对文本没有特殊要求,可以使用UILabel内部的实现方式,且需要放到子线程中进行,避免则塞主线程
计算文本宽高:
[NSAttributedString boundingRectWithSize:options:context:]
文本绘制:
[NSAttributedString drawWithRect:options:context:]
自定义文本控件,利用
TextKit
或最底层的CoreText
对文本异步绘制
。并且CoreText
对象创建好后,能直接获取文本的宽高等信息,避免了多次计算(调整和绘制都需要计算一次),CoreText直接使用了CoreGraphics
占用内存小,效率高
图片处理(解码+绘制)
当使用
UIImage
或CGImageSource
的方法创建图片时,图片的数据不会立即解码,而是在设置时解码(即图片设置到UIImageView/CALayer.contents
中,然后在CALayer
提交至GPU渲染前,CGImage
中的数据才进行解码)。这一步是无可避免
的,且是发生在主线程
中的。想要绕开这个机制,常见的做法是在子线程中优先将图片绘制到CGBitmapContext
,然后从Bitmap
直接创建图片,例如SDWebImage
三方框架对图片的编解码处理,这就是Image
的预解码
当使用
CG
开头的方法绘制图像
到画布中,然后从画布中创建图片时,可以将图像的绘制
在子线程
中进行
图片优化
尽量使用
PNG
格式图片,不适用JPEG
图片通过
子线程预解码,主线程渲染
,即通过Bitmap
创建图片,在子线程赋值 image优化图片大小,尽量避免动态缩放
尽量将多张图合并为一张进行显示
尽量
避免使用透明的view
,因为使用透明的view,会导致在GPU中计算像素时,会将透明view下层图层的像素也计算进来,即颜色混合
处理按需加载
,例如在TableView中滑动时不加载图片,使用默认占位图,而是在滑动停止时加载少使用
addView
给cell
动态添加view
GPU层面的优化
相对于CPU而言,GPU主要是接收CPU提交的 纹理+顶点
,经过一系列 transform
,最终 混合并渲染
,输出到 屏幕上
尽量
减少在短时间内大量图片的显示
,尽可能将多张图片合为一张显示
,主要是因为当有大量图片进行显示时,无论是CPU计算还是GPU的渲染,都是非常耗时的,很可能出现掉帧的情况尽量
避免图片的尺寸超过4096x4096
,因为当图片超过这个尺寸时,会先由CPU进行预处理,然后再提交给GPU处理,导致额外CPU资源消耗尽量
减少视图数量和层次
,主要是因为视图过多且重叠时,GPU会将其混合,混合的过程也是非常耗时的尽量避免离屏渲染
异步渲染
,例如可以将cell中的所有控件、视图合并成一张图片进行显示。可以参考 Graver 三方框架
注:上述这些优化方式的落地实现,需要根据自身项目进行评估,合理的使用进行优化
- Post title:OC底层原理38:界面优化方案
- Post author:张建
- Create time:2021-06-09 14:49:41
- Post link:https://redefine.ohevan.com/2021/06/09/OC底层原理/OC底层原理38:界面优化方案/
- Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.