一、UITableView的构建原理
在新闻类,电商类等应用中,应用着大量的图文混排视图,在表视图UITableView中,开发者通常需要在如下代理方法中计算出当前cell填充内容后的高度,之后将其返回:
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
//先根据数据源中数据计算高度
CGFloat height = 0;
return height;
}
然而,如果在如上方法中进行打印调试可以发现,heightForRowAtIndexPath方法会重复执行好多次,首先,并且heightForRowAtIndexPath方法的执行机制在不同版本的iOS系统还会有很大不同。以iOS9为例,一行cell要展示在屏幕上,至少要执行5遍TableView的heightForRowAtIndexPath方法:
TableView配置部分:
① 当TableView视图即将展现在屏幕上时,会把所有行的行高数据进行拉取。
②当TableView在执行setLayoutMargins方法进行自身布局时会把所有行高数据进行拉取。
③TableView在执行layoutSubViews方法进行子视图布局时会再次把所有行高数据进行拉取。
TableViewCell配置部分:
④当使用cellID进行与TableView绑定的cell获取时会拉取本行cell的高度数据。
⑤当cell进行layoutSubViews方法进行布局时会再次拉取本行cell的高度数据。
上面列举的5中拉取cell高度的场景中,TableView配置部分只会在TableView第一次展现在屏幕上时出现,但是其拉取的是所有行的行高数据,如果表视图有100行或者更多,这将是一个十分耗费性能的过程。TableViewCell配置部分,只有当cell将要出现在屏幕上时才会出现,并且只拉取当前行的行高,这两种场景会在用户滑动TableView时不断被执行,并且根据UITableView的布局cell原理,系统会默认准备当前一屏高度所能容纳cell个数加1个cell。
当执行TableView的reloadData方法进行界面刷新时,系统先会把所有行的行高数据拉取一遍,之后和UITableViewCell配置部分的场景一直,会拉取即将出现在屏幕上的cell的行高数据。
用示意图形象的表示上述逻辑如下:
通过上面分析,以10行数据的表格视图为例,若一屏幕可以呈现7行数据(TableView需要准备8行),则在第一次展示TableView视图时,会执行44次heightForRwoAtIndexPath方法,每次刷新TableView需要执行24次heightForRwoAtIndexPath方法,如果TableView的行数增加到3位数,则这个方法的执行次数将会十分恐怖👿。
至于为何UITableView在进行配置时也需要拉取所有的行高数据,我猜想其为了进行视图的一些初始化操作,例如表视图右侧滚动条的宽度和所占比例等。并且,每次拉取高度都从代理方法拉取,而不是存入内部的一个变量属性中,避免了因为数据源更改时机巧合而产生的界面与预期不一致的风险。
二、对UITableView可变行高的计算方式进行优化
通过前面的分析,可以理解如果将复杂的计算代码写在heightForRowAtIndexPath方法中,代价将是非常惨重的。滑动不流畅,屏幕卡顿很多性能问题都是由于这个原因。对于行高固定的表格视图,开发者可以直接设置TableView的固定行高,如下:
_tableView.rowHeight = 200;
如果行高是不固定了,则应该想办法让heightForRowAtIndexPath方法完成最少的工作,其实最少的工作莫过于拿过一个高度,直接返回,因此开发者通常会将对应行的行高计算一次后,把值进行保存,之后在执行heightForRowAtIndexPath方法拉取行高时,直接返回已经计算过的行高数据,具体如何操作比较灵活,可以对应一个数组属性,将计算后的行高放入数组中,每次取行高时,检查数组中是否已经有计算过的行高数据,如果有直接返回。我个人更倾向将行高数据封装进cell的数据模型Model中。
通过优化,可以有效的减少重复的高度计算,这也是我原先处理此类问题的主要方式。然而,只是提高了代码的性能,对开发者来说,工作量和复杂度有增而无减。在开发中通常会遇到一些十分复杂的界面,而这些界面中cell的高度都是需要通过请求到的数据动态改变的,每个cell都要写复杂的尺寸计算代码十分令人心烦。在iOS7之后,系统提供了一种自动计算cell高度的方法,这无论在性能还是工作量上,都完全解放了开发者。
在iOS7系统之后,UITableView类中增加了一个estimatedRowHeight属性,顾名思义,这个属性是设置UITableViewCell中的大约行高值。这个值设置之后,开发者无需设置rowHeight属性,也不需要实现heightForRowAtIndexPath方法,系统会自动根据UITableViewCell中contentView的约束来计算自己的行高。estimatedRowHeight属性用于TableView进行初始化,其会影响到表格视图右侧滚动条的宽度。cell展现出来时真正的行高并不受这个属性值的影响。