UITableView 性能优化 -- 《iOS 性能优化实战》读书笔记1

UITableView 的加载原理

可变高的列表载体 Cell 是在开发中经常处理到的一个技术点。
UITableViewCell 的高度需要在数据源代理中设置:

-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
    return height;
}

heightForRowAtIndexPath方法会重复执行很多次,并且heightForRowAtIndexPath方法的执行机制在不同版本 iOS 系统中还会有很大不同。在 iOS 11 中,默认已经采取了一些优化手段。

在 iOS 9上,要显示一行 cell,至少要执行 5 遍heightForRowAtIndexPath方法:

  • UITableView 配置部分
    • 当 UITableView 视图即将被展示在屏幕上时,会拉取所有行高数据
    • UITableView 在执行setLayoutMargins方法进行自身布局时会拉取所有行高数据
    • UITableView 在执行layoutSubviews方法进行子视图布局时会再次拉取所有行高数据
  • UITableViewCell 配置部分
    • 当使用 CellID 获取与 UITableView 绑定的 cell 时会拉取本行 cell 的高度数据
    • 当 cell 调用 layoutSubviews 方法急性布局时会再次拉取本行 cell 的高度数据

在上面列举的 5 种拉取 cell 高度数据的场景中,UITableView 配置部分只会在 UITableView 第一次展现在屏幕上时出现,但是它拉取的是所有行的行高数据,如果表视图有 100 行或者更多,那么这就是一个非常耗能的过程。

UITableViewCell 的配置部分,只有当 cell 将要出现在屏幕上时才会出现,并且只拉取当前 cell 的行高,这两种场景会在用户滑动 UITableView 时不断被执行,并且根据 UITableView 的 cell 布局原理,系统会默认准备比当前一屏高度所能容纳cell 的个数多 1 个 cell。

当执行reloadData方法进行界面刷新时,系统先会把所有行的行高数据拉取一遍,之后和 UITableViewCell 配置部分的场景一致,会拉取即将出现在屏幕上的 cell 的行高数据。下面是UITableView 的加载原理示意图:

UITableView 加载原理图

通过以上分析,以 10 行数据的视图为例,若一屏幕可以呈现 7 行数据(UITableView 需要准备 8 行),则在第一次展示 UITableView 视图时,会执行 44 次heightForRowAtIndexPath,每次刷新 UITableView 需要执行 24 次heightForRowAtIndexPath,如果 UITableView 行数增加的三位数,这个方法的执行次数将会非常惊人。

可变行高的优化方式

如果将复杂的计算代码写在heightForRowAtIndexPath中,代价是巨大的。滑动不流畅、屏幕卡顿等很多性能问题都是由于这个原因。对于行高固定的表格视图,可以直接设置 UITableView 的固定行高,如果行高是不固定的,则应该想办法让heightForRowAtIndexPath方法完成最少的工作。

其实最少的工作莫过于拿一个高度直接返回,因此通常会将对应行的行高计算一次后,把值保存起来,之后在执行heightForRowAtIndexPath拉取行高时,直接返回。具体操作比较灵活,可以对应一个数组属性,将计算后的行高放入数组,在每次取行高时,检查数组中是否有已经计算过的行高数据,如果有直接返回。一种更好的方式是将行高数据封装进 cell 的数据模型 Model 中。

然而,这只是提高了代码性能,工作量和复杂度有增无减。iOS 7 之后,可以使用estimateRowHeight属性,iOS 11 中,系统已经默认定义这个值为 44 来进行列表视图的性能优化。这个值设置 cell 的大约行高。设置estimateRowHeight无需再设置rowHeight,也不需要实现heightForRowAtIndexPath,系统会根据 cell 中的 contentView 的约束来计算自己的行高。estimateRowHeight用于 UITableView的初始化,会影响到表格视图右侧滚动条的宽度。当 cell 展现出来时,真正的行高并不受这个属性值的影响。

So,问题来了:如何让cell 正确计算自己的高度?
答案是使用 AutoLayout,给 cell 布局足够的压力,让 contentView 的上下左右必须被内部控件的约束撑满。

Note:cell 的子视图必须添加到 contentView 上,否则计算时会出现问题

这是性能最优的列表渲染方式。

上面说预估行高会影响到 UITableView 右侧滚动条的展示,如果每个 cell 行高跳跃跨度比较大,则滚动条宽度的配置会失准,随着用户滑动,右侧滚动条可能会出现长短跳跃的情况,如果想要精准这个滚动条的配置,可以在如下代理方法中返回具体 cell 的估计行高:

- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
    //这里根据不同分区或者不同行设置估计的行高
    return 44;
}

关于estimatedHeightForRowAtIndexPath这个方法还有一种应用场景:
对于没有使用自动布局、cell 的高度需要手动计算的场景,如果实现了这个方法,并且实现了heightForRowAtIndexPath方法,那么heightForRowAtIndexPath方法会以懒加载的方法执行,只有在 cell 将要被展现在屏幕上时才会被执行,这也可以有效减少由于高度计算带来的性能负担。

Note: UITableViewCell在被创建出来时,宽度并不一定和 UITableView 的宽度一致,如果需要获取 cell 的绝对宽度来处理逻辑,那么需要在 cell的 layoutSubviews 方法里面进行,此时 cell 的宽度才正确

高度不固定的列表分区头、尾视图

一般头尾高度固定,不考虑它们带来的性能问题。对比 cell 的布局原理,也可以使用自动布局实现自适应高度。

你可能感兴趣的:(UITableView 性能优化 -- 《iOS 性能优化实战》读书笔记1)