卡顿产生的原因
在屏幕成像的过程中,CPU和GPU的职责及
CPU:负责对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制(Core Graphics
)。
GPU:负责变换、合成、纹理的渲染。
CPU 把计算好的数据给 GPU,GPU 来渲染,渲染后的数据放在帧缓存(缓冲区,有两块缓冲区,前帧缓存和后帧缓存,协调使用,效率高)中。然后,视频控制器从缓冲区获取渲染后的数据显示在屏幕上。
图像显示原理
引用YY大神对于图像显示原理的分析
一帧(或一页)数据就是:一个垂直同步信号(VSync
)和一个水平同步信号(HSync
)的组合。先发送一个垂直同步信号(VSync
),代表即将显示一页,再发送一个水平同步信号(HSync
)就显示一帧。如果当下一次VSync信号到来之前,CPU和GPU还没有计算完成,就会产生卡顿。
在 VSync 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 App,App 主线程开始在 CPU 中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等。随后 CPU 会将计算好的内容提交到 GPU 去,由 GPU 进行变换、合成、渲染。随后 GPU 会把渲染结果提交到帧缓冲区去,等待下一次 VSync 信号到来时显示到屏幕上。由于垂直同步的机制,如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。
从上面的图中可以看到,CPU 和 GPU 不论哪个阻碍了显示流程,都会造成掉帧现象。所以开发时,也需要分别对 CPU 和 GPU 压力进行评估和优化。
卡顿的优化
从上述结论中可以得出,造成卡顿的原因是由CPU和GPU造成的,所有优化的时候也要从这两个方面来着手。那么解决卡顿的主要思路就是尽可能减少CPU、GPU资源消耗,按照60FPS的刷新帧率,每隔16ms就会有一次VSync信号。
CPU优化
-
对象创建:尽量用轻量级的对象,比如用不到事件处理的地方,可以考虑使用
CALayer
取代UIView
。尽量推迟对象创建时间,并把对象的创建分到多个任务中去。如果对象可以复用,可以将对象放入缓存池中复用,这样cpu消耗会很小。 -
对象调整:不要频繁地调整
UIView
的布局相关属性, 比如frame
、bounds
、center
、transform
等属性,尽量避免调整视图层次、添加和移除视图。 -
布局计算:尽量在后台提前计算好布局、并且对布局进行缓存,会提高很多性能。因为对这些属性调整会非常消耗资源,需一次性调整好,不要多次、频繁的计算修改这些属性。
-
Autolayout:对于复杂视图
Autolayout
会比直接设置frame
消耗更多的CPU
资源。设置frame可以用一些工具(比如常用的:left/right/top/bottom/width/height 快捷属性),或使用ComponentKit、AsyncDisplayKit 等框架。 -
文本处理:文本的宽高计算会占用一部分资源,可以参数UILabel内部实现,用:
[NSAttributedString boundingRectWithSize:options:context:]
来计算文本宽高,用[NSAttributedString drawWithRect:options:context:]
来绘制文本。为了不占用主线程资源,尽量放到后台处理。 -
图片处理:图片在创建为UIImage时不会立即解码,在设置到UIImageView中,CALayer被提交到GPU中时才会去解码并且会发生在主线程。为了绕开这个机制,会在后台先把图片绘制到CGBitmapContext 中,然后从 Bitmap 直接创建图片。目前常见的网络图片库都自带这个功能。图片的绘制过程也可以放到子线程进行,比如常见的
[UIView drawRect:]
,原理如下:- (void)display { dispatch_async(backgroundQueue, ^{ CGContextRef ctx = CGBitmapContextCreate(...); // draw in context... CGImageRef img = CGBitmapContextCreateImage(ctx); CFRelease(ctx); dispatch_async(mainQueue, ^{ layer.contents = img; }); }); }
-
图片的
size
最好刚好跟UIImageView
的size
保持一致。 -
控制子线程的最大并发数量。
GPU优化
- 纹理渲染:
GPU
能处理的最大纹理尺寸是4096x4096
,一旦超过这个尺寸,就会占用CPU
资源进行预处理,这对CPU和GPU都会带来额外的资源消耗。 所以纹理尽量不要超过这个尺寸。 - 视图混合:GPU会将多个视图混合在一起,如果视图结构复杂会消耗过多GPU资源。所以,应该尽量减少视图数量和层次,尽量避免短时间内大量图片的显示,尽可能将多张图片合成一张进行显示。并在不透明的视图里标明 opaque 属性以避免无用的 Alpha 通道合成。
- 图形生成:设置CALayer 的border、圆角、阴影、遮罩(mask)通常都会触发离屏渲染,而离屏渲染通常是发生在GPU中。在开发中尽量减少这个属性的使用,可以尝试开启
CALayer.shouldRasterize
的属性,会把原本离屏渲染操作转嫁到CPU上。对应只需要圆角的某些场合,可以用一张已经绘制好的圆角图片覆盖到原本视图上面来模拟相同的视觉效果。也可以把需要显示的图形在后台线程绘制为图片。
卡顿检测
- 主线程卡顿监控。通过子线程监测主线程的 runLoop,判断两个状态区域之间的耗时是否达到一定阈值。具体原理和实现,这篇文章介绍得比较详细。
- FPS监控。要保持流畅的UI交互,App 刷新率应该当努力保持在 60fps。监控实现原理比较简单,通过记录两次刷新时间间隔,就可以计算出当前的 FPS。
做性能优化的时候,不要过早的优化。根据业务需求,对流畅度要求高的地方优先进行优化,走修改代码- > Profile(测量)-> 修改代码的流程来逐步优化。