前言
UITableView是我们开发APP最常使用到的控件之一,使用它可以向用户展示大量的数据和信息,为了把这些数据更好的展示给用户,tableview的样式可能会变得非常复杂但美观。在我们制作出一个好看的tableview的同时,性能问题也逐渐走入了我们的视线。
为了让tableview的在美观的同时保持良好的性能,我们需要了解影响tableview性能的原因还有如何优化这些问题。
首先我们要知道,我们滑动时看到界面卡顿是由CPU和GPU开销共同决定的(参考每日一问02——渲染流程)
1.cell重用
一个tableview中往往会存在很多cell,如果每显示一个cell都要重新加载,那需要的开销将会是非常庞大的。苹果已经为我们提供了最基本的cell重用机制。
//将cell加载到内存
- (void)registerNib:(nullable UINib *)nib forCellReuseIdentifier:(NSString *)identifier NS_AVAILABLE_IOS(5_0);
- (void)registerClass:(nullable Class)cellClass forCellReuseIdentifier:(NSString *)identifier NS_AVAILABLE_IOS(6_0);
//使用内存中可重用的cell
- (nullable __kindof UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier;
2.高度计算
当只有一种高度的情况下,我们可以直接指定cell的高度,这样是没有性能影响的。但很多时候会因为内容换行,图片等不确定因素导致行高不一致。这个时候就需要计算行高或者让系统计算行高。
>动态计算高度
iOS7后,系统给我们提供了estimatedRowHeight这个属性,让我们预估一个cell的高度由系统根据具体的约束计算cell高度。
self.tableView.estimatedRowHeight = 80;
>手动计算高度
很多时候我们会通过cell内控件的布局,约束,文字高度来计算这个cell的具体高度。我们会在代理方法heightForRowAtIndexPath
中返回cell的具体高度。
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
return [cell getHeight];
}
这样做虽然解决了动态高度cell的展示,但会因为每加载一个cell就会进行一次计算导致CPU计算量增加,如果cell中布局非常复杂,那这个消耗是很大的。
>缓存计算高度
对于计算过一次的高度来讲,我们不要再次计算,所以我们可以把计算好的高度进行缓存。保存再model里。当需要展示同一个cell时就可以直接使用上一次计算出来的高度。
3.渲染
渲染是由CPU和GPU共同完成的,在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃。就造成了我们看到的卡顿现象。而CPU消耗主要在各种计算上,图形图像的计算主要则是靠GPU完成的。所以我们需要让CPU和GPU利用率尽可能的均衡。
>减少GPU开销
- 尽量使用不透明的视图进行布局
当多个透明图像重叠时,GPU会进行计算,合成出一个颜色,这样的开销无疑是巨大的。所以在没有特殊需求的时候,我们可以设置layer的opaque来关闭合成,使用简单的拷贝图层而不考虑这个图层下面的东西。 - 减少离屏渲染
离屏渲染主要在两个地方开销较大:
1.创建新缓冲区,要想进行离屏渲染,首先要创建一个新的缓冲区。
2.上下文切换
我们要知道,CPU和GPU都可能造成离屏渲染。但我们可以让CPU和GPU负载均衡来保证渲染的性能。例如Core Graphics的所有API都会造成CPU的离屏渲染。
造成离屏渲染的主要方式
- cornerRadius(圆角)
- masks(遮罩)
- shadows(阴影)
- edge antialiasing(抗锯齿)
- group opacity(不透明)
我们遇到的最多的就是添加圆角造成的离屏渲染,解决方案就是使用CPU来绘制圆角图片:参考(每日一问06——imageView的圆角优化)
>减少CPU开销
- 图片预渲染
当我们使用imageWithContentsOfFile
加载一张图片时,图片只是被加载到了内存中并没有被渲染到UIimageView上,当UIImageView进行渲染时才会在主线程将这个图片解压成位图,这个解压的过程是一个非常耗时的CPU操作。
解决方案就是:在子线程预先对图片进行强制解压,生成可用的位图(每日一问04-加载图片对性能的影响)
4.异步
我们都知道UI操作是在主线程执行的,所以我们可以尽可能避免在主线程进行资源加载或对数据进行计算。把这些操作放在子线程中进行再显示在UI上。最常见的例子就是异步加载网络图片并显示。
5.减少多余的计算
很多时候tableview中会加载很多cell,而屏幕中只能展示其中那么几个。所以在滑动的过程中,如果我们只是在cellForRowAtIndexPath
中对每一个cell都开线程进行图片或其他资源的加载,那么消耗将会非常大。
解决方案就是在快速滑动过程中,不执行异步加载,当滚动开始减速的时候才加载显示在当前屏幕上的cell。
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"];
//如果没有取到,就初始化
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"Cell"];
}
cell.textLabel.text = @"Name";
BOOL canLoad = !self.tableView.dragging && !_tableView.decelerating;
if (canLoad) {
//开始loaddata,异步加载图片
NSLog(@"开始加载图片");
}
return cell;
}
// 滚动停止时,触发该函数
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
//刷新tableview
// [self.tableView reloadData];
//在此刷新的是屏幕上显示的cell的内容
NSArray *visiblePaths = [self.tableView indexPathsForVisibleRows];
//获取到的indexpath为屏幕上的cell的indexpath
[self.tableView reloadRowsAtIndexPaths:visiblePaths withRowAnimation:UITableViewRowAnimationRight];
}
6.使用统一的cell
有的时候,我们界面上会存在许多样式不同,但布局方式很相似的cell。这时我们可以只编写一个cell样式,通过不同需求改变cell样式。这样做的好处有两点
1.减少代码量,减少Nib文件的数量,统一一个Nib文件定义Cell,容易修改、维护。
2.基于Cell的重用,真正运行时铺满屏幕所需的Cell数量大致是固定的,设为N个。所以如果如果只有一种Cell,那就是只有N个Cell的实例;但是如果有M种Cell,那么运行时最多可能会是“M x N = MN”个Cell的实例
在改变cell样式时,我们应尽量避免动态添加,移除subView,这样的操作会带来很多额外的计算。我们应该使用hidden方式直接隐藏和显示subView,这样要比重新创建subView性能高得多。
相关文章
提升UITableView性能-复杂页面的优化
优化UITableViewCell高度计算的那些事
iOS UITableView性能优化
iOS Tableview优化