iOS UI性能优化
一、FPS
1、概念
FPS(Frames Per Second),是指画面每秒传输帧数。通俗来讲就是指动画或视频的画面数,每秒钟帧数越多,所显示的动作就会越流畅。通常,要避免动作不流畅的最低是30,iOS的优化极限则是60。
二、显示
1、显示原理
计算机显示的流程大致可以描述为将图像转化为一系列像素点的排列然后打印在屏幕上,由图像转化为像素点的过程又可以称之为光栅化,就是从矢量的点线面的描述,变成像素的描述。屏幕显示图像的原理如图:
ios_screen_scan.png
首先从过去的 CRT 显示器原理说起。CRT 的电子枪按照上面方式,从上到下一行行扫描,扫描完成后显示器就呈现一帧画面,随后电子枪回到初始位置继续下一次扫描。为了把显示器的显示过程和系统的视频控制器进行同步,显示器(或者其他硬件)会用硬件时钟产生一系列的定时信号。当电子枪换到新的一行,准备进行扫描时,显示器会发出一个水平同步信号(horizonal synchronization),简称 HSync;而当一帧画面绘制完成后,电子枪回复到原位,准备画下一帧前,显示器会发出一个垂直同步信号(vertical synchronization),简称 VSync。尽管现在的设备大都是液晶显示屏了,但原理仍然没有变。
ios_screen_display.png
计算机系统中 CPU、GPU、显示器是以上图这种方式协同工作的。CPU 计算好显示内容提交到 GPU,GPU 渲染完成后将渲染结果放入帧缓冲区,随后视频控制器会按照 VSync 信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示。
2、iOS显示
从软件层面上,iOS借助Core Graohics,Core Animation,Core Image完成图形的处理,它们又都是借助OpenGL ES来完成底层的工作,其结构如下图所示:
arch01.png
Display 的上一层便是图形处理单元 GPU,GPU 是一个专门为图形高并发计算而量身定做的处理单元。这也是为什么它能同时更新所有的像素,并呈现到显示器上。它并发的本性让它能高效的将不同纹理合成起来。因为涉及到各种图形矩阵的计算,它跟CPU最直观的区别在于浮点计算能力要超出CPU很多。所以在开发中,我们应该尽量让CPU负责主线程的UI调动,把图形显示相关的工作交给GPU来处理。
3、屏幕撕裂
生成图像的设备(如GPU)与显示图像的设备(如显示器)是分离的。显示器的刷新频率是固定的,而显卡的生成图像的频率是变化的。当GPU还在渲染下一帧图像时,显示器却已经开始进行绘制,这样就会导致屏幕撕裂(Screen Tearing)。这会使得屏幕中一部分显示的是上一帧的内容,另一部分显示的是下一帧的内容。
Screen Tearing
如果显示器的刷新频率与 GPU 的渲染速度完全相同,应该就会解决屏幕撕裂的问题了吧?其实并不是。显示器从 GPU 拷贝帧的过程依然需要消耗一定的时间,如果屏幕在拷贝图像时刷新,仍然会导致屏幕撕裂问题。
4、缓冲区
引入缓冲区可以有效地缓解屏幕撕裂,也就是同时使用一个帧缓冲区(frame buffer)和一个或者多个后备缓冲区(back buffer),在每次显示器请求内容时,都会从帧缓冲区中取出图像然后渲染。
在最简单的情况下,帧缓冲区只有一个,这时帧缓冲区的读取和刷新都都会有比较大的效率问题。为了解决效率问题,显示系统通常会引入两个缓冲区,即双缓冲机制。在这种情况下,GPU 会预先渲染好一帧放入一个缓冲区内,让视频控制器读取,当下一帧渲染好后,GPU 会直接把视频控制器的指针指向第二个缓冲器。如此一来效率会有很大的提升。
虽然缓冲区可以减缓这些问题,但是却不能解决;如果后备缓冲区绘制完成,而帧缓冲区的图像没有被渲染,后备缓冲区中的图像就会覆盖帧缓冲区,仍然会导致屏幕撕裂。
5、 V-Sync
垂直同步(Vertical synchronization),简称 V-Sync ,主要作用就是保证只有在帧缓冲区中的图像被渲染之后,后备缓冲区中的内容才可以被拷贝到帧缓冲区中。
在 VSync 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 App,App 主线程开始在 CPU 中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等。随后 CPU 会将计算好的内容提交到 GPU 去,由 GPU 进行变换、合成、渲染。随后 GPU 会把渲染结果提交到帧缓冲区去,等待下一次 VSync 信号到来时显示到屏幕上。由于垂直同步的机制,如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。
ios_frame_drop.png
从上面的图中可以看到,CPU 和 GPU 不论哪个阻碍了显示流程,都会造成掉帧现象。所以开发时,也需要分别对 CPU 和 GPU 压力进行评估和优化。
三、CPU vs GPU
1、CPU(中央处理器)
1、对象创建
对象的创建会分配内存、调整属性、甚至还有读取文件等操作,比较消耗 CPU 资源。尽量用轻量的对象代替重量的对象,可以对性能有所优化。
通过 Storyboard 创建视图对象时,其资源消耗会比直接通过代码创建对象要大非常多。
2、对象调整
对象的调整也经常是消耗 CPU 资源的地方。对 UIView 的属性进行调整时,消耗的资源要远大于一般的属性。对此应该尽量减少不必要的属性修改。
当视图层次调整时,UIView、CALayer 之间会出现很多方法调用与通知,所以在优化性能时,应该尽量避免调整视图层次、添加和移除视图。
3、布局计算
视图布局的计算是 App 中最为常见的消耗 CPU 资源的地方,如果能在后台线程提前计算好视图布局、并且对视图布局进行缓存,那么这个地方基本就不会产生性能问题了。
4、Autolayout
Autolayout 是苹果本身提倡的技术,在大部分情况下也能很好的提升开发效率,但是 Autolayout 对于复杂视图来说常常会产生严重的性能问题。随着视图数量的增长,Autolayout 带来的 CPU 消耗会呈指数级上升。具体数据可以看这个文章:http://pilky.me/36/。 如果你不想手动调整 frame 等属性,你可以用一些工具方法替代(比如常见的 left/right/top/bottom/width/height 快捷属性),或者使用 ComponentKit、AsyncDisplayKit 等框架。
5、文本计算
如果一个界面中包含大量文本(比如微博微信朋友圈等),文本的宽高计算会占用很大一部分资源,并且不可避免。如果你对文本显示没有特殊要求,可以参考下 UILabel 内部的实现方式:用 [NSAttributedString boundingRectWithSize:options:context:]
来计算文本宽高,用 -[NSAttributedString drawWithRect:options:context:]
来绘制文本。尽管这两个方法性能不错,但仍旧需要放到后台线程进行以避免阻塞主线程。
2、GPU(图形处理器)
相对于 CPU 来说,GPU 能干的事情比较单一:接收提交的纹理(Texture)和顶点描述(三角形),应用变换(transform)、混合并渲染,然后输出到屏幕上。
1、offscreen rendering(离屏渲染)
这发生在当不能直接在屏幕上绘制,并且必须绘制到离屏图片的上下文中的时候。离屏绘制发生在基于CPU或者是GPU的渲染,或者是为离屏图片分配额外内存,以及切换绘制上下文,这些都会降低GPU性能。对于特定图层效果的使用,比如圆角,图层遮罩,阴影或者是图层光栅化都会强制Core Animation提前渲染图层的离屏绘制。
2、视图的混合
当多个视图(或者说 CALayer)重叠在一起显示时,GPU 会首先把他们混合到一起。如果视图结构过于复杂,混合的过程也会消耗很多 GPU 资源。为了减轻这种情况的 GPU 消耗,应用应当尽量减少视图数量和层次,并在不透明的视图里标明 opaque 属性以避免无用的 Alpha 通道合成。
四、离屏渲染
1、概念
On-Screen Rendering:意为当前屏幕渲染,指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行。
Off-Screen Rendering:意为离屏渲染,指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。
2、离屏渲染原因
3、为何要避免离屏渲染
WWDC 2011 Understanding UIKit Rendering 指出一般导致图形性能的问题大部分都出在了offscreen rendering,因此如果我们发现列表滚动不流畅,动画卡顿等问题,就可以想想和找出我们哪部分代码导致了大量的offscreen 渲染。
离屏渲染主要在两个地方开销较大:
1、创建新缓冲区
要想进行离屏渲染,首先要创建一个新的缓冲区。
2、上下文切换
离屏渲染的整个过程,需要多次切换上下文环境:先是从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上有需要将上下文环境从离屏切换到当前屏幕。而上下文环境的切换是要付出很大代价的。
4、如何检测离屏渲染
1、模拟器
Simulator -> Debug -> Color Off-screen Rendered
离屏渲染的图层会变成黄色
offScreen.png
2、 Instruments – Core Animation
3、Instruments – OpenGL ES
5、触发离屏渲染的属性
1、cornerRadius+masksToBounds
首先设置圆角最简单的方法是调用cornerRadius,官方文档中描述cornerRadius只作用于background color and border of the layer
,所以如果有内容需要设置 masksToBounds 为YES裁剪内容。
view.layer.cornerRadius = 10.f;
eg
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0.f, 20.f, 100, 100.f)];
view.backgroundColor = [UIColor redColor];
view.layer.cornerRadius = 50.f;
[self.view addSubview:view];
上文代码并没有触发离屏渲染,所以结论:视图设置圆角,如果没有内容,一般来说仅指定cornerRadius即可;如果有内容,需指定masksToBounds,并进行实际裁剪,从而产生离屏渲染。
产生离屏渲染的例子:
// 1、UIView添加子视图,cornerRadius+masksToBounds+subview
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0.f, 20.f, 100, 100.f)];
view.backgroundColor = [UIColor redColor];
view.layer.cornerRadius = 50.f;
view.layer.masksToBounds = YES;
[self.view addSubview:view];
UIView *subview = [[UIView alloc] initWithFrame:view.bounds];
subview.backgroundColor = [UIColor greenColor];
[view addSubview:subview];
// 2、UILabel添加标题,cornerRadius+masksToBounds+backgroundColor+borderWidth
UILabel * label = [[UILabel alloc] initWithFrame:CGRectMake(150, 50.f, 100.f, 100.f)];
label.backgroundColor = [UIColor blueColor];
label.text = @"UILabelUILabelUILabelUILabelUILabelUILabelUILabelUILabel";
label.numberOfLines = 0;
label.layer.borderWidth = 5.f;
label.layer.borderColor = [UIColor redColor].CGColor;
label.layer.cornerRadius = 50.f;
label.layer.masksToBounds = YES;
[self.view addSubview:label];
// 3、UIButton添加标题,cornerRadius+masksToBounds+backgroundColor+title
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
button.frame = CGRectMake(0, 100, 200, 40.f);
button.backgroundColor = [UIColor redColor];
[button setTitle:@"button" forState:UIControlStateNormal];
button.layer.cornerRadius = 20.f;
button.layer.masksToBounds = YES;
[self.view addSubview:button];
// 4、UIImageView添加图片,cornerRadius+masksToBounds+backgroundColor+image
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 300.f, 100.f, 100.f)];
imageView.backgroundColor = [UIColor redColor];
imageView.image = [UIImage imageNamed:@"jd"];
imageView.layer.cornerRadius = 50.f;
imageView.layer.masksToBounds = YES;
[self.view addSubview:imageView];
// 5、UITextView添加文字,cornerRadius+masksToBounds?+backgroundColor?+text
UITextView *textView = [[UITextView alloc] initWithFrame:CGRectMake(0, 450.f, [UIScreen mainScreen].bounds.size.width, 100.f)];
textView.backgroundColor = [UIColor orangeColor];
textView.text = @"UITextViewUITextViewUITextViewUITextViewUITextViewUITextViewUITextViewUITextViewUITextViewUITextViewUITextViewUITextViewUITextViewUITextViewUITextViewUITextViewUITextViewUITextViewUITextViewUITextViewUITextViewUITextViewUITextViewUITextView";
textView.layer.cornerRadius = 50.f;
// textView.layer.masksToBounds = YES;
[self.view addSubview:textView];
综上所述,圆角图形关于离屏渲染的优化,不一定非得去写贝塞尔曲线:
1、UIView,一般用来实现纯色圆角视图,尽量不要添加不透明的子视图,设置layer.cornerRadius
添加圆角,不设置masksToBounds
即可避免离屏渲染
2、UILabel,一般用来实现圆角背景文本框或者有圆角边框的文本框,与UIView相比较有些特殊,仅设置backgroundColor
和layer.cornerRadius
不显示圆角,猜想backgroundColor
是绘制在内容上了,设置masksToBounds
后可出现圆角且并没有触发离屏渲染。
实现圆角背景文本框:只要不添加border
,不会触发离屏渲染。
有圆角边框的文本框,设置borderWidth
触发离屏渲染,取消backgroundColor
和masksToBounds
可避免离屏渲染。
3、UIButton,一般是要设置背景色和标题,设置cornerRadius添加圆角,不设置masksToBounds即可避免离屏渲染
4、UIImageView,一般要设置图片,设置cornerRadius添加圆角,设置masksToBounds裁剪内容,不设置背景色即可避免离屏渲染
1、shadow
添加阴影触发离屏渲染,设置shadowPath
,提前告诉CoreAnimation要渲染的View的形状,可避免触发离屏渲染
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(50.f, 50.f, 100, 100.f)];
view.backgroundColor = [UIColor redColor];
view.layer.shadowColor = [UIColor blueColor].CGColor;
view.layer.shadowOffset = CGSizeMake(10.f, 10.f);
view.layer.shadowOpacity = 1;
view.layer.shadowPath = [UIBezierPath bezierPathWithRect:view.layer.bounds].CGPath;
[self.view addSubview:view];
参考:
1、脑洞大开:为啥帧率达到 60 fps 就流畅
2、提升 iOS 界面的渲染性能
3、CPU VS GPU
4、iOS 保持界面流畅的技巧
5、iOS图形原理与离屏渲染
6、绘制像素到屏幕上