iOS UITableView优化
一、Cell 复用
在可见的页面会重复绘制页面,每次刷新显示都会去创建新的 Cell,非常耗费性能。
解决方案:创建一个静态变量 reuseID,防止重复创建(提高性能),使用系统的缓存池功能。
static NSString * CELL_RUID = @"CELL"; // 调用次数太多,static 保证只创建一次 reuseID,提高性能
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// 缓存池中取已经创建的 cell
UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:CELL_RUID
forIndexPath:indexPath];
return cell;
}
通过 identifier 标识不同类型的 cell,缓存池中只会保存已经被移出屏幕的不同类型的 cell。
- (nullable __kindof UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier; // Used by the delegate to acquire an already allocated cell, in lieu of allocating a new one.
- (__kindof UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath NS_AVAILABLE_IOS(6_0); // newer dequeue method guarantees a cell is returned and resized properly, assuming identifier is registered
复用 Cell 时 不会调用 awakeFromNib。
- 获取方法的区别
dequeueReusableCellWithIdentifier:forIndexPath 如果没有注册复用 identifier,执行这句时会崩溃,提示:
reason: 'unable to dequeue a cell with identifier CELL - must register a nib or a class for the identifier or connect a prototype cell in a storyboard'
dequeueReusableCellWithIdentifier 如果没有注册复用 identifier,语句返回 nil,继续执行会崩溃。提示:
failed to obtain a cell from its dataSource
判断 nil 后可以自己创建 cell。
{
MyCell * cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"];
if (cell == nil) {
cell = [[MyCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"Cell"];
}
}
- 为什么需要 forIndexPath:
因为在返回 cell 之前,会调用委托 tableView:heightForRowAtIndexPath:来确定 cell 尺寸(如果已经定义该函数)。
我们经常在 tableView:cellForRowAtIndexPath: 中为每一个 cell 绑定数据,实际上在调用 cellForRowAtIndexPath: 的时候 cell 还没有被显示出来,为了提高效率我们应该把数据绑定的操作放在 cell 显示出来后再执行,可以在 tableView:willDisplayCell:forRowAtIndexPath: 方法中绑定数据。
注意 willDisplayCell 中 cell 在 tableview 展示之前就会调用,此时 cell 实例已经生成,所以不能更改 cell 的结构,只能是改动 cell 上的 UI 的一些属性,如 label 的内容、控件的隐藏等。
二、定义一种(尽量少)类型的 Cell 及善用 hidden 隐藏(显示)subviews
分析 Cell 结构,尽可能的将相同内容的抽取到一种样式 Cell 中。UITableView 真正创建出的 Cell 可能只比屏幕显示的多一点。虽然 Cell 的"体积"可能会大点,但是因为 Cell 的数量不会很多,完全可以接受的。
好处:
①、减少代码量,减少 Nib 文件的数量,在一个 Nib 文件定义 Cell,容易修改、维护;(多个 Cell 不是更容易维护?)
②、基于复用机制,真正运行时铺满屏幕所需的 Cell 数量大致是固定的,设为 N 个。如果只有一种 cell,那就是只有 N + c 个 cell 的实例;但是如果有 M 种 cell,那么运行时最多可能会是 M * (N + c) 个 cell 的实例,虽然这可能并不会占用太多内存,但能少一些更好。
既然只定义一种 Cell,那么需要把所有不同类型的 view 都定义好,放在 Cell 里面,通过 hidden 属性控制,来显示不同类型的内容。毕竟,在用户快速滑动中,只是单纯的显示/隐藏 subview 比实时创建要快得多。
尽量少用 [cell addSubview:] 动态添加 View,可以初始化时就添加,然后通过 hidden 属性来控制。
三、提前计算并缓存 Cell 的高度
3.1 固定高度的 cell
self.tableView.rowHeight = 88;
直接采用上面方式给定高度,不需要实现 tableView:heightForRowAtIndexPath: 以节省不必要的计算和开销。
3.2 动态高度的 cell
实现代理方法后,上面的 rowHeight 属性的设置将会变成无效。
tableView:estimatedHeightForRowAtIndexPath: -> tableView:heightForRowAtIndexPath: 获取每个 Cell 即将显示的高度,从而确定表格视图的布局,实际是要获取滚动视图的 contentSize,然后调用 tableView:cellForRowAtIndexPath:,获取每个 Cell,进行赋值。如果有很多个 Cell 要显示,那么方法会执行很多次。
解决方案:在 Model(Entity)中计算并保存 Cell 的高度。其实 Model 中保存 UI 的参数是很奇怪的,最好放在 MVVM 模式的 ViewModel(视图模型)中,让 Model(数据模型)只负责处理数据。
@interface Model : NSObject
@property (nonatomic, assign) CGFloat cellHeight; // Cell 高度
/**
* @brief 计算高度
*/
- (void)calculateCellHeight;
@end
在 tableView:heightForRowAtIndexPath: 中尽量不使用 cellForRowAtIndexPath: 方法来获取 cell,如果你需要用到它,只用一次然后缓存结果。
还可以继续进行优化,提前创建真正显示的、需要加工的数据并缓存。如:接口返回 NSString 而展示 NSAttributeString。
四、异步绘制(自定义 Cell 绘制)
遇到比较复杂的界面时(复杂点的图文混排),上面缓存行高的方式可能就不能满足要求了。详细整理:UITableView 优化技巧
/**
* @brief cell 添加 draw 方法
*/
- (void)draw
{
// 异步绘制
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
});
}
/**
* @brief 重写 drawRect: 方法
*/
- (void)drawRect:(CGRect)rect
{
// 不需要用 GCD 异步线程,因为 drawRect: 本来就是异步绘制的。
}
绘制的各个信息都是根据之前算好的布局进行绘制的。这里是需要异步绘制。
五、滑动时,按需加载
自定义 Cell 的种类千奇百怪,但它本来就是用来显示数据的,差不多 100% 带有图片,这个时候就要考虑,下滑的过程中可能会有点卡顿,尤其网络不好的时候,异步加载图片是个程序员都会想到,但是如果给每个循环对象都加上异步加载,开启的线程太多,一样会卡顿。这个时候利用 UIScrollViewDelegate 两个代理方法就能很好地解决这个问题。
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (needLoadArr.count > 0 && [needLoadArr indexOfObject:indexPath] == NSNotFound) {
[cell clear]; // 清掉内容
}
return cell;
}
// 按需加载 - 如果目标行与当前行相差超过指定行数,只在目标滚动范围的前后指定 3 行加载。
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{
NSIndexPath * ip = [self.tableView indexPathForRowAtPoint:CGPointMake(0, targetContentOffset->y)];
NSIndexPath * cip = [[self.tableView indexPathsForVisibleRows] firstObject];
NSInteger skipCount = 8;
// -8 < 当前位置 - 目标位置 < 8
if (labs(cip.row - ip.row) > skipCount) {
// 目标区域的 cell 的 indexPaths
NSArray * temp = [self.tableView indexPathsForRowsInRect:CGRectMake(0, targetContentOffset->y, self.tableView.frame.size.width, self.tableView.frame.size.height)];
NSMutableArray * arr = [NSMutableArray arrayWithArray:temp];
if (velocity.y < 0) {
NSIndexPath * indexPath = [temp lastObject];
if (indexPath.row + 33) {
[arr addObject:[NSIndexPath indexPathForRow:indexPath.row - 3 inSection:0]];
[arr addObject:[NSIndexPath indexPathForRow:indexPath.row - 2 inSection:0]];
[arr addObject:[NSIndexPath indexPathForRow:indexPath.row - 1 inSection:0]];
}
}
[needLoadArr addObjectsFromArray:arr];
}
}
思想:识别 UITableView 拖拽即将结束的时候,进行异步加载图片,快滑动过程中,只加载目标范围内的 Cell,这样按需加载,极大的提高流畅度。而 SDWebImage 可以实现异步加载,与这条性能配合就完美了,尤其是大量图片展示的时候。而且也不用担心图片缓存会造成内存警告的问题。
六、缓存 View
当 Cell 中的部分 View 是非常独立且不便于重用的,"体积"非常小,在内存可控的前提下,完全可以将这些 view 缓存起来。
七、尽量显示“大小刚好合适的”图片资源
避免大量的图片缩放、颜色渐变等。
八、避免同步的从网络、文件获取数据
Cell 内实现的内容来自 web,使用异步加载,缓存请求结果。
九、渲染
减少 subviews 的个数和层级
子控件的层级越深,渲染到屏幕上所需要的计算量就越大;如多用 drawRect 绘制元素,替代用 view 显示。
少用 subviews 的透明图层
渲染最耗时的操作之一就是混合(blending)了。对于不透明的 View,设置 opaque = YES,这样在绘制该 View 时,避免 GPU 对 View 覆盖的其他内容也进行绘制。
背景色不要使用 clearColor
避免 CALayer 特效(shadowPath)
给 Cell 中 View 加阴影会引起性能问题,如下面代码会导致滚动时有明显的卡顿:
view.layer.shadowColor = color.CGColor; view.layer.shadowOffset = offset;
view.layer.shadowOpacity = 1;
view.layer.shadowRadius = radius;当有图像时,预渲染图像,在 bitmap context 先将其画一遍,导出成 UIImage 对象,然后再绘制到屏幕,这会大大提高渲染速度。具体内容可以自行查找“利用预渲染加速显示 iOS 图像”相关资料。
十、总结
UITableView 的优化主要从四个方面入手:
- 提前计算并缓存好高度(布局),因为 tableView:heightForRowAtIndexPath: 是调用最频繁的方法;
- 滑动时按需加载,防止卡顿。这个在大量图片展示,网络加载的时候很管用,配合 SDWebImage;
- 异步绘制,遇到复杂界面,遇到性能瓶颈时,可能就是突破口;
- 缓存一切可以缓存的,这个在开发的时候,往往是性能优化最多的方向。
大概需要关注的:
- cell 复用
- cell 高度的计算
- 渲染(混合问题)
- 减少视图的数目(重写 drawRect:)
- 减少多余的绘制操作
- 不要给 cell 动态添加 subView
- 异步化 UI,不要阻塞主线程
- 滑动时按需加载对应的内容
十一、资料
图片加载优化官方 Demo:LazyTableImages