iOS之武功秘籍⑳: 界面优化
写在前面
我们经常在面试中,会被问及关于界面优化相关的问题,比如为什么界面会出现卡顿?如何监控卡顿?接着如何解决卡顿?那么本篇文章将重点分析一下卡顿的原理和解决的措施.
一、界面卡顿
通常来说,计算机中的显示过程是下面这样的,通过CPU
、GPU
、显示器
协同工作来将图片显示到屏幕上
- 1、
CPU
计算好显示内容,提交至GPU
- 2、
GPU
经过渲染完成后将渲染的结果放入FrameBuffer
(帧缓存区) - 3、随后
视频控制器
会按照VSync
信号逐行读取FrameBuffer
的数据 - 4、经过可能的
数模转换
传递给显示器进行显示
最开始时,FrameBuffer
只有一个,这种情况下FrameBuffer
的读取和刷新有很大的效率问题,为了解决这个问题,引入了双缓存区
.即双缓冲机制
.在这种情况下,GPU
会预先渲染好一帧
放入FrameBuffer
,让视频控制器读取,当下一帧渲染好后,GPU
会直接将视频控制器的指针指向第二个FrameBuffer
.
双缓存机制虽然解决了效率问题,但是随之而言的是新的问题,当视频控制器还未读取完成时,例如屏幕内容刚显示一半,GPU
将新的一帧内容提交到FrameBuffer
,并将两个FrameBuffer
进行交换后,视频控制器就会将新的一帧数据的下半段显示到屏幕上,造成屏幕撕裂
现象.
为了解决这个问题,采用了垂直同步信号机制
.当开启垂直同步后,GPU
会等待显示器的VSync
信号发出后,才进行新的一帧渲染和FrameBuffer
更新.而目前iOS设备中采用的正是双缓存区+VSync
.
屏幕卡顿原因
下面我们来说说,屏幕卡顿的原因.
在VSync
信号到来后,系统图形服务会通过 CADisplayLink
等机制通知 App
,App
主线程开始在CPU
中计算显示内容.随后 CPU
会将计算好的内容提交到 GPU
去,由GPU
进行变换、合成、渲染.随后 GPU
会把渲染结果提交到帧缓冲区
去,等待下一次 VSync
信号到来时显示到屏幕上.由于垂直同步的机制,如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示
,而这时显示屏会保留之前的内容不变.所以可以简单理解掉帧
为过时不候
.
如下图所示,是一个显示过程,第1帧在VSync
到来前,处理完成,正常显示,第2帧在VSync
到来后,仍在处理中,此时屏幕不刷新,依旧显示第1帧,此时就出现了掉帧
情况,渲染时就会出现明显的卡顿现象
.
从图中可以看出,CPU
和GPU
不论是哪个阻碍了显示流程,都会造成掉帧现象
,所以为了给用户提供更好的体验,在开发中,我们需要进行卡顿检测
以及相应的优化
.
二、卡顿监控
卡顿监控的方案一般有两种:
-
FPS监控
:为了保持流畅的UI
交互,App
的刷新拼搏应该保持在60fps
左右,其原因是因为iOS设备
默认的刷新频率是60次/秒
,而1次刷新(即VSync
信号发出)的间隔是1000ms/60 = 16.67ms
,所以如果在16.67ms
内没有准备好下一帧数据,就会产生卡顿 -
主线程卡顿监控
:通过子线程监测主线程的RunLoop
,判断两个状态(kCFRunLoopBeforeSources
和kCFRunLoopAfterWaiting
)之间的耗时是否达到一定阈值
FPS监控
FPS
的监控,参照YYKit
中的YYFPSLabel
,主要是通过CADisplayLink
实现.借助link
的时间差,来计算一次刷新所需的时间,然后通过 刷新次数 / 时间差
得到刷新频次,并判断是否其范围,通过显示不同的文字颜色来表示卡顿严重程度
.如果只是简单的监测,使用FPS
足够了.
主线程卡顿监控
除了FPS
,还可以通过RunLoop
来监控,因为卡顿的是事务,而事务是交由主线程
的RunLoop
处理的.
实现思路:检测主线程每次执行消息循环的时间,当这个时间大于规定的阈值时,就记为发生了一次卡顿.这个也是微信卡顿三方matrix
的原理.
卡顿检测三方库:
-
Swift
的卡顿检测第三方ANREye,其主要思路是:创建子线程进行循环监测,每次检测时设置标记置为true,然后派发任务到主线程,标记置为false,接着子线程睡眠超过阈值时,判断标记是否为false,如果没有,说明主线程发生了卡顿 -
OC
可以使用 微信matrix、滴滴DoraemonKit
三、界面优化
CPU层面的优化
-
1、尽量
用轻量级的对象
代替重量级的对象,可以对性能有所优化,例如 不需要相应触摸事件的控件,用CALayer
代替UIView
-
2、尽量减少对
UIView
和CALayer
的属性修改-
CALayer
内部并没有属性,当调用属性方法时,其内部是通过运行时resolveInstanceMethod
为对象临时添加一个方法,并将对应属性值保存在内部的一个Dictionary
中,同时还会通知delegate
、创建动画等,非常耗时 -
UIView
相关的显示属性,例如frame
、bounds
、transform
等,实际上都是从CALayer
映射来的,对其进行调整时,消耗的资源比一般属性要大
-
-
3、当有大量对象释放时,也是非常耗时的,尽量挪到后台线程去释放
-
4、尽量
提前计算视图布局
,即预排版
,例如cell
的行高 -
5、
Autolayout
在简单页面情况下们可以很好的提升开发效率,但是对于复杂视图而言,会产生严重的性能问题,随着视图数量的增长,Autolayout
带来的CPU
消耗是呈指数上升的.所以尽量使用代码布局
.如果不想手动调整frame
等,也可以借助三方库,例如Masonry(OC)、SnapKit(Swift)、ComponentKit、AsyncDisplayKit等
-
6、文本处理的优化:当一个界面有大量文本时,其行高的计算、绘制也是非常耗时的
-
1)如果对文本没有特殊要求,可以使用
UILabel
内部的实现方式,且需要放到子线程中进行,避免阻塞主线程- 计算文本宽高:
[NSAttributedString boundingRectWithSize:options:context:]
- 文本绘制:
[NSAttributedString drawWithRect:options:context:]
- 计算文本宽高:
-
2)自定义文本控件,利用
TextKit
或最底层的CoreText
对文本异步绘制.并且CoreText
对象创建好后,能直接获取文本的宽高等信息,避免了多次计算(调整和绘制都需要计算一次).CoreText
直接使用了CoreGraphics
占用内存小,效率高
-
-
7、图片处理(解码 + 绘制)
-
当使用
UIImage
或CGImageSource
的方法创建图片时,图片的数据不会立即解码
,而是在设置时解码(即图片设置到UIImageView/CALayer.contents
中,然后在CALayer
提交至GPU
渲染前,CGImage
中的数据才进行解码).这一步是无可避免
的,且是发生在主线程
中的.想要绕开这个机制,常见的做法是在子线程中先将图片绘制到CGBitmapContext
,然后从Bitmap
直接创建图片,例如SDWebImage
三方框架中对图片编解码的处理。这就是Image
的预解码 -
当使用
CG
开头的方法绘制图像到画布中,然后从画布中创建图片时,可以将图像的绘制在子线程中进行
-
-
8、图片优化
- 尽量使用
PNG
图片,不使用JPGE
图片 - 通过
子线程预解码,主线程渲染
,即通过Bitmap
创建图片,在子线程赋值image
- 优化图片大小,尽量避免动态缩放
- 尽量将多张图合为一张进行显示
- 尽量使用
-
9、尽量避免使用透明
view
,因为使用透明view
,会导致在GPU
中计算像素时,会将透明view
下层图层的像素也计算进来,即颜色混合处理
-
10、按需加载,例如在
TableView
中滑动时不加载图片,使用默认占位图,而是在滑动停止时加载 -
11、少使用
addView
给cell
动态添加view
GPU层面优化
相对于CPU
而言,GPU
主要是接收CPU
提交的纹理+顶点
,经过一系列transform
,最终混合并渲染,输出到屏幕上.
-
1、尽量
减少在短时间内大量图片的显示
,尽可能将多张图片合为一张显示
,主要是因为当有大量图片进行显示时,无论是CPU
的计算还是GPU
的渲染,都是非常耗时的,很可能出现掉帧的情况 -
2、尽量避免图片的尺寸超过
4096×4096
,因为当图片超过这个尺寸时,会先由CPU
进行预处理,然后再提交给GPU
处理,导致额外CPU
资源消耗 -
3、尽量减少视图数量和层次,主要是因为视图过多且重叠时,
GPU
会将其混合,混合的过程也是非常耗时的 -
4、尽量避免离屏渲染
-
5、异步渲染,例如可以将
cell
中的所有控件、视图合成一张图片进行显示.可以参考Graver三方框架
提示
上述这些优化方式的落地实现,需要根据自身项目进行评估,合理的使用进行优化
写在后面
和谐学习,不急不躁.我还是我,颜色不一样的烟火.