卡顿产生的原因
在 VSync 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 App,App 主线程开始在 CPU 中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等。随后 CPU 会将计算好的内容提交到 GPU 去,由 GPU 进行变换、合成、渲染。随后 GPU 会把渲染结果提交到帧缓冲区去,等待下一次 VSync 信号到来时显示到屏幕上。由于垂直同步
的机制,如果在一个VSync
时间内,CPU
或者 GPU
没有完成内容提交,则那一帧就会被丢弃
,等待下一次机会再显示,而这时显示屏会保留之前的内容不变
。这就是界面卡顿
的原因。
从上面的图中可以看到,CPU 和 GPU 不论哪个阻碍了显示流程,都会造成掉帧现象。所以开发时,也需要分别对 CPU 和 GPU 压力进行评估和优化。
CPU 资源消耗原因和解决方案
1.对象创建、调整、销毁
对象创建
对象的创建会分配内存、调整属性、甚至还有读取文件等操作,比较消耗 CPU 资源。尽量用轻量的对象
代替重量的对象
,可以对性能有所优化。比如CALayer
比UIView
要轻量许多,那么不需要响应触摸事件的控件,用 CALayer 显示会更加合适。如果对象不涉及 UI 操作,则尽量放到后台线程去创建,但可惜的是包含有 CALayer 的控件,都只能在主线程创建和操作。通过 Storyboard 创建视图对象时,其资源消耗会比直接通过纯代码
创建对象要大非常多,在性能敏感的界面里,Storyboard 并不是一个好的技术选择。
尽量推迟对象创建(懒加载)
的时间,并把对象的创建分散到多个任务中去。尽管这实现起来比较麻烦,并且带来的优势并不多,但如果有能力做,还是要尽量尝试一下。如果对象可以复用
,并且复用的代价比释放、创建新对象要小,那么这类对象应当尽量放到一个缓存池里复用
对象调整
对象的调整也经常是消耗 CPU 资源的地方。这里特别说一下CALayer
:CALayer 内部并没有属性,当调用属性方法时,它内部是通过运行时 resolveInstanceMethod 为对象临时添加一个方法,并把对应属性值保存到内部的一个 Dictionary 里,同时还会通知 delegate、创建动画等等,非常消耗资源。UIView 的关于显示相关的属性(比如 frame/bounds/transform)等实际上都是 CALayer 属性映射来的,所以对 UIView 的这些属性进行调整时,消耗的资源要远大于一般的属性。对此你在应用中,应该尽量减少不必要的属性修改
。
当视图层次调整时,UIView、CALayer 之间会出现很多方法调用与通知,所以在优化性能时,应该尽量避免调整视图层次
、添加和移除视图
。
对象销毁
对象的销毁虽然消耗资源不多,但累积起来也是不容忽视的。通常当容器类持有大量对象时,其销毁时的资源消耗就非常明显。同样的,如果对象可以放到后台线程去释放,那就挪到后台线程去。这里有个小 Tip:
- 把对象捕获到 block 中,然后扔到后台队列去随便发送个消息以避免编译器警告,就可以让对象在后台线程销毁了。
- 大次数循环的对象创建可放在@autoreleasepool,及时释放,避免内存溢出
NSArray *tmp = self.array;
self.array = nil;
dispatch_async(queue, ^{
[tmp class];
});
2.预排版(布局计算、文本计算、缓存高度等等)
布局计算
尽量减少视图的布局计算,应当在后台提前计算好视图布局
,并且对视图布局进行缓存。(参考上面,不要多次、频繁的计算和调整视图的这些属性)
Autolayout
Autolayout
虽能提高开发效率,但随着视图数量增长,Autolayout会严重影响CPU的性能。如果你不想手动调整frame,可以使用ComponentKit、AsyncDisplayKit等框架。你可以用一些工具方法替代(比如常见的 left/right/top/bottom/width/height 快捷属性),或者使用 ComponentKit、AsyncDisplayKit
等框架。
文本计算
如果一个界面中包含大量文本(比如微博微信朋友圈等),文本的宽高计算会占用很大一部分资源,并且不可避免。如果你对文本显示没有特殊要求,可以参考下 UILabel 内部的实现方式:用
[NSAttributedString boundingRectWithSize:options:context:]
来计算文本宽高,用-[NSAttributedString drawWithRect:options:context:]
来绘制文本。尽管这两个方法性能不错,但仍旧需要放到后台线程
进行以避免阻塞主线程。
如果你用CoreText
绘制文本,那就可以先生成 CoreText 排版对象,然后自己计算了,并且 CoreText 对象还能保留以供稍后绘制使用。
高度缓存
在tableView
滑动时,会不断调用heightForRowAtIndexPath:
,当cell
高度需要自适应时,每次回调都要计算高度,会导致 UI 卡顿。为了避免重复无意义的计算,需要缓存高度。
怎么缓存?
- 字典,NSCache。
- UITableView-FDTemplateLayoutCell,国人开发优化计算 UITableViewCell 高度并缓存的轻量级框架
3.预渲染(文本等异步绘制,图片解码,图像绘制等可以放到子线程)
文本渲染
屏幕上能看到的所有文本内容控件,包括 UIWebView,在底层都是通过 CoreText 排版、绘制为 Bitmap 显示的。常见的文本控件 (UILabel、UITextView 等),其排版和绘制
都是在主线程
进行的,当显示大量文本时,CPU 的压力会非常大。对此解决方案只有一个,那就是自定义文本控件,用 TextKit 或最底层的 CoreText 对文本异步绘制
。尽管这实现起来非常麻烦,但其带来的优势也非常大,CoreText 对象创建好后,能直接获取文本的宽高等信息,避免了多次计算(调整 UILabel 大小时算一遍、UILabel 绘制时内部再算一遍);CoreText 对象占用内存较少
,可以缓存下来以备稍后多次渲染。
图片的解码
当你用UIImage 或 CGImageSource 的相关方法创建图片时,应当在后台线程
先把图片绘制到CGBitmapContext
中,从 Bitmap 直接创建图片。(目前常见的网络图片库都做了这个处理)。
图像的绘制
图像绘制尽量放到后台线程,因为CoreGraphic方法通常是线程安全的,图像显示时再回到主线程。一个简单异步绘制
的过程大致如下(实际情况会比这个复杂得多,但原理基本一致):
- (void)display {
dispatch_async(backgroundQueue, ^{
CGContextRef ctx = CGBitmapContextCreate(...);
// draw in context...
CGImageRef img = CGBitmapContextCreateImage(ctx);
CFRelease(ctx);
dispatch_async(mainQueue, ^{
layer.contents = img;
});
});
}
GPU 资源消耗原因和解决方案
纹理渲染
尽量不要图片和视图的大小超过机器的纹理尺寸;
避免短时间内大量图片的显示,如果可以,尽可能将多张图片合成为一张进行显示。(比如TableView
存在非常多的图片并且快速滑动时不展示)
1.通过UIScrollView的代理方法
2.利用runloop的Mode
视图混合
应该尽量减少视图
数量
和层次
,并在不透明
的视图里标明opaque=YES
以避免无用的Alpha通道合成。
图形生成
避免离屏渲染
什么是离屏渲染
(1)On-Screen Rendering (当前屏幕渲染)
指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区进行。
(2)Off-Screen Rendering (离屏渲染)
指的是在GPU在当前
屏幕缓冲区
以外开辟
一个缓冲区进行渲染操作
离屏渲染耗性能体现在两个方面:
(1)创建新缓冲区
要想进行离屏渲染,首先要创建一个新的缓冲区。
(2)上下文切换
离屏渲染的整个过程,需要多次切换
上下文环境:先是从当前屏幕(On-Screen)切换到离屏(Off-Screen),等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上有需要将上下文环境从离屏切换到当前屏幕。而上下文环境的切换是要付出很大代价的。
下面的情况或操作会引发离屏渲染:
- 为图层设置
圆角
cornerRadius+clipsToBounds
解决方法:CoreGraphics
异步
自定义绘制
圆角,然后在主线程
上更新
- 为图层设置(
光栅化
)layer.shouldRasterize=true) - 为图层设置
遮罩
(layer.mask) - 为图层设置
阴影
(layer.shadow *) - edge antialiasing(
抗锯齿
) - group opacity(
不透明
)
将图层layer.allowsGroupOpacity属性设置为YES和layer.opacity小于1.0
- 渐变
参考文章
YY
大神的 iOS 保持界面流畅的技巧
离屏渲染