性能优化:屏幕卡顿优化
一、屏幕成像原理及屏幕卡顿原因
二、屏幕卡顿优化
三、定量监测屏幕FPS
四、定位卡顿效果
五、定位耗时代码
六、果然好客服功能,聊天界面滑动优化
一、屏幕成像原理及屏幕卡顿原因
屏幕成像原理:iOS设备的屏幕成像,是由CPU和GPU协作完成的。CPU(中央处理器)主要负责计算要显示的内容:
- 比如创建视图、销毁视图、计算视图的位置、大小、背景色
- 比如计算文本的位置、大小、颜色
- 比如图片的格式转换、解码
- 比如图像的绘制(Core Graphics框架)
而GPU(图形处理器)主要负责渲染要显示的内容。
那么当显示器想要显示内容时,就会发出一个VSync
(垂直同步信号,iOS设备每秒会发出60个VSync
——即iOS设备的屏幕刷新频率为60Hz),系统会立马开始让CPU计算要显示的内容,计算完后,把计算结果发送给GPU;GPU渲染要显示的内容,渲染完后,把渲染结果缓存到帧缓存中;等下一个VSync
到来时,CPU计算、GPU渲染下一帧的同时,视频控制器会从帧缓存中读取一帧的渲染结果显示到屏幕上。

屏幕卡顿原因:所以一旦下一个VSync
到来时,因为CPU或GPU的压力过大,没有及时渲染完下一帧缓存到帧缓存中,那么屏幕就会继续显示上一帧,这就导致屏幕该刷新的时候没刷新,出现滞留现象——这就是我们常说的屏幕卡顿或者屏幕掉帧。

二、屏幕卡顿优化
所以我们做屏幕卡顿优化,主要就是从减轻CPU和GPU的压力入手,尽可能地保证在下一个VSync
到来时,CPU和GPU能够协作完成下一帧的渲染并缓存到了帧缓存中。
1、视图方面
- 尽可能减少视图的层级和数量,如不必要的嵌套就去掉,使用懒加载创建视图等;尽可能使用轻量级对象,比如用不到事件处理的地方可以考虑用
CALayer
替代UIView
- 不要频繁地修改视图的属性,同一时间段同一属性的多次修改最好合并成一次修改,如
frame
、bounds
、transform
等,因为我们每修改一次CPU都要重新计算一次 - 尽可能减少透明视图的使用(
alpha
< 1),因为一旦有透明度,多个透明视图重叠的部分(如颜色)就需要额外地计算和渲染
2、文本、图片方面
- 不要频繁地修文本的属性,同一时间段同一属性的多次修改最好合并成一次修改
- 最好直接使用
PNG
格式,而不是JPEG
,这样就不需要CPU做额外的格式转换 - 图片的
size
最好跟UIImageView
的size
保持一致,这样就不需要CPU做额外的伸缩操作 - 短时间内要显示大量图片时,尽可能把多张图片合成一张图片来显示,以此减少图片的解码、计算、渲染单位
3、耗时代码方面
- 尽量把一些耗时的操作放到子线程,比如文本尺寸的计算、图片解码的操作等,来减小CPU的压力
4、尽可能避免离屏渲染
4.1 离屏渲染是什么?
On-Screen Rendering
:在屏渲染,即GPU的渲染结果直接存储在帧缓存中供下一帧显示。
Off-Screen Rendering
:离屏渲染,即GPU的渲染结果无法直接存储在帧缓存中时,系统就会调用CPU开辟一块新缓存来存储渲染结果。
4.2 为什么要尽可能避免离屏渲染?
正常情况下就是在屏渲染,即GPU每渲染一帧就缓存到帧缓存中供下一帧显示。但在某些情况下,GPU的渲染结果无法直接存储在帧缓存中,于是系统就会调用CPU开辟一块新缓存来存储渲染结果。这样视频控制器要读取离屏渲染缓存时就要把上下文切换到新缓存去读取,要读取在屏渲染缓存时又要把上下文切换到帧缓存去读取,这样反复的上下文切换非常消耗CPU。
所以从开辟新缓存和反复切换上下文这两点来看,我们就知道离屏渲染很有可能导致CPU计算、GPU渲染超过1/60s,从而导致屏幕卡顿,所以要尽可能避免离屏渲染。
4.2 什么情况下会触发离屏渲染?
-
设置圆角:
layer.cornerRadius > 0
&&layer.masksToBounds = YES
。(这两个条件必须同时满足才会触发离屏渲染,只使用其中一个是不会触发离屏渲染的) - 设置阴影:
layer.shadowXXX
。 - 设置遮罩:
layer.mask
。 -
设置光栅化:
layer.shouldRasterize
。(所谓光栅化是指当layer.shouldRasterize
设为YES
时,layer
就会被CPU渲染成位图并缓存,下次使用时直接读取复用,但是这个缓存如果100ms没人使用就会被移除,而且改变设置了光栅化layer
的内容时会造成离屏渲染)
三、定量监测屏幕FPS
屏幕FPS的理想值为60,也就是说当屏幕一秒刷新60次时,用户滑动界面是感觉不到卡顿的。而根据苹果官方的说法,当屏幕FPS低于45时,用户滑动界面就会感觉到明显的卡顿。
Instruments
的Core Animation
:
-
Xcode
打开项目,连接真机(一定要用真机调试,因为模拟器用的是电脑的CPU,检测不出手机的屏幕卡顿来)。 - 打开
Instruments
的Core Animation
。



- 选择真机、选择项目、运行、手指不移出屏幕滑动界面、定量地监测屏幕FPS。
// ViewController.m
@interface ViewController () <UITableViewDataSource, UITableViewDelegate>
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
UITableView *tableView = [[UITableView alloc] initWithFrame:[UIScreen mainScreen].bounds style:(UITableViewStylePlain)];
tableView.backgroundColor = [UIColor whiteColor];
tableView.dataSource = self;
tableView.delegate = self;
[tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"cellReuseID"];
[self.view addSubview:tableView];
}
#pragma mark - UITableViewDataSource, UITableViewDelegate
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 100;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cellReuseID" forIndexPath:indexPath];
cell.textLabel.text = [NSString stringWithFormat:@"%ld", indexPath.row];
return cell;
}
@end


我们在cellForRowAtIndexPath
方法里添加一些耗时操作。
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cellReuseID" forIndexPath:indexPath];
cell.textLabel.text = [NSString stringWithFormat:@"%ld", indexPath.row];
// 模拟耗时操作
for (int i = 0; i < 2000; i++) {
NSLog(@"%d", i);
}
return cell;
}

四、定位卡顿效果

打开Xcode
,把项目运行在真机上,然后可以用下面常用的属性去定位卡顿效果。
-
Color Blended Layers
:设置了透明效果的视图 -
Color Misaligned Images
:出现了图片伸缩的imageView
-
Color Offscreen-Rendered Yellow
:出现了离屏渲染的地方 -
Color Hits Green and Misses Red
:设置了光栅化的视图 - ......
五、定位耗时代码
此处推荐一个三方库LXDAppFluecyMonitor
来定位耗时代码。

六、果然好客服功能,聊天界面滑动优化
当然屏幕滑动优化的方案有很多,比如尽量减少透明视图的使用;尽量让图片的大小和UIImageView一样大,减少图片伸缩;尽量减少切圆角导致的离屏渲染;尽量把一些耗时操作放到子线程中,如文本宽高的计算等。
但是经过定量检测,我们发现影响我们聊天界面FPS的主要原因就是一个,那就是“滑动界面时,会一直计算聊天文本Label
的高度和聊天气泡Cell
的高度”,其它三个原因对聊天界面FPS的影响微乎其微。
所以我们就定了主要的优化方案就是针对耗时操作,优化之前聊天界面的FPS在53左右,而优化之后FPS就变成了57~59。具体的做法就是,请求完聊天数据后,我们会立马开辟一个子线程去计算聊天文本的高度,这样也计算出了每个Cell
的高度,把它们和聊天数据一起封装成一个Cell
布局对象。这样TableView
在频繁地调用heightForRowAtIndexPath
方法时,就不用一直计算Cell
的高度了,同时把这个Cell
布局对象设置到Cell
内部时,Cell
内部也不用一直计算聊天内容Label
的高度了,这可以很大程度地提升聊天界面的FPS。