iOS 中 UITableView 的优化

UITableView的性能优化

UITableView 是 iOS 开发中经常会用到的一种控件,复杂的 UITablView 如果不进行优化,会严重影响用户体验。那么我们可以从那些方面如后来对其进行优化呢?

首先 我们使用 UITableView 的大致流程如下 :

  • 获取将要在 UITableView 中显示的数据;
  • 把数据进行处理,封装为相应的 Model,并存入数组
  • UITableView 调用 reloadData 刷新数据
  • 在代理方法中获取或创建 UITableViewCell ,并将数据赋值给它
  • UITableViewCell 根据拿到的数据对其中的 UI 控件进行赋值
  • 在代理方法中计算 UITableViewCell 的行高
  • UITableViewCell 布局子控件

我们的优化就是针对这几个步骤来进行的。

针对 UITableView 的数据处理的优化

需要显示在 UITableView 中的数据一般是通过网络请求从服务器获得的数据,我们需要发起网络请求获得数据后进行处理,以使其能够适合我们要显示的 Cell。然而网络请求和数据处理都是比较耗时的操作,将它们在后台异步线程中进行处理完成后,再来刷新 UITableView 会使得显示更加流畅。

关于 UITableView 的重用机制

UITableView 只会创建在当前屏幕显示或多一点的 UITableViewCell,其它的都是从中取出来重用的。每当 Cell 不再屏幕中显示时,系统将其放入一个集合中,一般称为重用池。当要显示某一位置的 Cell 时,会优先去重用池中取,如果重用池中存在,则直接拿来显示,如果没有才会创建。这极大的减少了内存的开销。

UITableView 最主要的两个回调方法是 tableView:cellForRowAtIndexPath:tableView:heightForRowAtIndexPath: 。在显示 Cell 时,UITableView 显示多次调用 tableView:heightForRowAtIndexPath: 以确定 contentSize 及 Cell 的位置,然后才会调用 tableView:cellForRowAtIndexPath: ,来显示当前屏幕上的 Cell。

UITableView 的显示优化实际上主要就是针对这两个回调方法的优化。

UITableViewCell 高度计算

固定高度的 Cell 的显示自不必说,直接设置固定的高度即可:

1
self.tableView.rowHeight	=	HEIGHT_OF_CELL;

而对于动态高度的 Cell 来说,则需要在 tableView:heightForRowAtIndexPath: 方法中给出相应的高度。动态高度的计算依赖于要显示在 Cell 中的数据内容,我们可以直接在处理数据时就进行高度的计算,而不是在 tableView:heightForRowAtIndexPath: 方法中进行计算,这也会节省一些时间。

1
2
3
4
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
DataModel *model = self.dataSourceList[indexPath.row];
returen model.heightOfCell;
}

如果不提前进行 Cell 高度的计算,将已经计算好的高度进行缓存,避免重复进行计算也是一个可以优化的地方。

UITableViewCell 的内容布局

对象的调整也经常是消耗 CPU 资源的地方。这里特别说一下 CALayer:CALayer 内部并没有属性,当调用属性方法时,它内部是通过运行时 resolveInstanceMethod 为对象临时添加一个方法,并把对应属性值保存到内部的一个 Dictionary 里,同时还会通知 delegate、创建动画等等,非常消耗资源。UIView 的关于显示相关的属性(比如 frame/bounds/transform)等实际上都是 CALayer 属性映射来的,所以对 UIView 的这些属性进行调整时,消耗的资源要远大于一般的属性。对此你在应用中,应该尽量减少不必要的属性修改。

对于布局内容不固定的 Cell 来说,根据要显示的数据内容进行内容的布局即可,如果要进行布局的控件很多,则会极大的消耗系统的资源。

而实际上我们 Cell 添加控件时,实质上都是系统调用底层的接口进行绘制,因此对于控件较多的动态 Cell 来说,我们可以直接进行绘制,提高效率。

直接在 drawRect 方法中进行绘制:

1
2
3
- (void)drawRect {
//
}

或将绘制好的内容作为图片返回后直接显示 :

1
2
3
4
5
6
7
8
9
- (UIImage *)draw {
// 内容绘制
CGContextRef context = UIGraphicsGetCurrentContext();
···
// 需要注意的是,绘制是比较耗时的操作,放入异步后台线程进行任务比较好
UIImage *contentImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return contentImage;
}

异步处理耗时操作

获取数据并对其进新处理等耗时操作,应该放入后台线程异步处理,等到数据完成处理后再通知主线程刷新 UI。

另外,UIKit 和 CoreAnimation 相关的操作必须在主线程中进行,其它诸如图像的绘制等则应该在后台线程异步执行。

在 Cell 上添加系统控件时,实质上系统都需要调用底层的接口进行绘制,当 Cell 上有大量的控件需要添加时,对资源的开销也会很大,直接绘制将会大大地提高效率。在遇到需要显示复杂的 UITableViewCell 时,使用自定义绘制会更好。

避免频繁的创建对象

对象的创建会产生内存分配、属性调整等操作,频繁大量的创建对象会消耗大量的资源和时间,应尽量避免频繁大量的创建对象。应该使用轻量对象来代替重量对象,或将对象创建后进行缓存。

减少对象的属性赋值操作

对象的调整也经常是消耗 CPU 资源的地方。这里特别说一下 CALayer:CALayer 内部并没有属性,当调用属性方法时,它内部是通过运行时 resolveInstanceMethod 为对象临时添加一个方法,并把对应属性值保存到内部的一个 Dictionary 里,同时还会通知 delegate、创建动画等等,非常消耗资源。UIView 的关于显示相关的属性(比如 frame/bounds/transform)等实际上都是 CALayer 属性映射来的,所以对 UIView 的这些属性进行调整时,消耗的资源要远大于一般的属性。对此你在应用中,应该尽量减少不必要的属性修改。

简化视图结构

GPU在绘制图像前,会把重叠的视图进行混合,视图结构越复杂,这个操作就越耗时,如果存在透明视图,混合过程会更加复杂。

减少离屏渲染

下面的情况或操作会引发离屏渲染:

  • 为图层设置遮罩(layer.mask)
  • 将图层的 layer.masksToBounds / view.clipsToBounds 属性设置为 true
  • 将图层 layer.allowsGroupOpacity 属性设置为 YES 和 layer.opacity 小于1.0
  • 为图层设置阴影(layer.shadow *)。
  • 为图层设置 layer.shouldRasterize = true
  • 具有 layer.cornerRadius,layer.edgeAntialiasingMask,layer.allowsEdgeAntialiasing 的图层
  • 文本(任何种类,包括UILabel,CATextLayer,Core Text等)。
  • 使用 CGContext 在 drawRect : 方法中绘制大部分情况下会导致离屏渲染,甚至仅仅是一个空的实现

按需加载

作者

Y2hlbmdsZWk=

发布于

2017-04-06

更新于

2021-09-01

许可协议