从自适应单元格高度说起-浅谈如何提高UITableView的加载效率

锦妖和她的小伙伴们 2018-03-01

大家基本上都做过这样的需求:在UITableView上展示文本,且文本内容长短不一,每一行单元格都要动态计算高度,使得单元格可以刚好容纳下需要展示的文字。为了方便讲解,我们把文本框设定成一个距离cell上下左右均有20px间距的UILabel,需要单元格动态调整高度,使得文本框刚好可以展示出所有的文本内容。

实现方案

需求本身并不是非常复杂,实现这个需求基本上可以采用两种方法:

1、代码动态计算高度

2、利用iOS8中UITableView的estimatedRowHeight新特性通过约束计算高度

我们先来看一下两种方案的实现方式:

代码动态计算高度

在UITableViewCell的自定义类中增加一个计算cell高度的类方法,具体代码如下:

+ (CGFloat)calculateTitleWidth:(NSString *)title{
    
    CGFloat stringWidth = 0;
    CGSize size = CGSizeMake(kRBScreenWidth - 20.0f*2, MAXFLOAT);
    
    if (title.length > 0) {
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000
        stringWidth = [title
                      boundingRectWithSize:size
                      options:NSStringDrawingUsesLineFragmentOrigin
                      attributes:@{NSFontAttributeName:kRBTextFont}
                      context:nil].size.height;
#else
        //iOS7.0以下方法
        stringWidth = [title sizeWithFont:kRBTextFont
                            constrainedToSize:size
                                lineBreakMode:NSLineBreakByCharWrapping].height;
#endif
    }
    return stringWidth;
}

当我们通过- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath方法得到对应的cell之后,调用cell的- (void)buildData:(NSString *)title方法,填充文本,设置文本框高度:

- (void)buildData:(NSString *)title{
    
    self.titleLabel.text = title;
    self.titleLabel.frame = CGRectMake(20.0f, 20.0f, kRBScreenWidth - 20.0f*2, [RBAutoSizeTableViewCell calculateTitleWidth:title]);
}

重写UITableViewDataSource的protocol方法,动态计算每一行的高度:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
    
    return [RBAutoSizeTableViewCell calculateTitleWidth:self.titles[indexPath.row]] + 20.0*2;
}

利用自动布局和约束计算高度结合estimatedRowHeight特性计算高度

先将titleLabel利用约束固定在cell上:

[self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.left.mas_equalTo(self.contentView).with.mas_offset(20.0f);
        make.bottom.right.mas_equalTo(self.contentView).with.mas_offset(-20.0f);
}];

再将UITableView设置为预估高度的模式:

self.estimatedRowHeight = 300.0f;  //设置近似值
self.rowHeight = UITableViewAutomaticDimension;

只需要两行代码,我们就完成了动态高度的估算工作,非常的简洁明了。对于自适应单元格高度感兴趣的同学可以下载我的Demo探索研究一下。

这里我用了Xib加载cell和代码构建cell两种方式生成cell:

//代码创建cell
if(!autoSizeTableViewCell){
        autoSizeTableViewCell = [[RBAutoSizeTableViewCell1 alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellID];
}

//nib创建cell
if(!autoSizeTableViewCell){
        autoSizeTableViewCell = [[NSBundle mainBundle] loadNibNamed:NSStringFromClass([RBAutoSizeTableViewCell2 class]) owner:self options:nil].lastObject;
}

尽管很多同学都用过Xib文件,但是对于其中的原理不甚熟悉,Xib其实就是一个XML文件,在项目运行时会被编译成二进制文件即nib文件,Fabric将会在下文中分析Xib的执行效率。

注意:千万不要再次重写- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath方法,否则UITableView将不会预估高度

加载效率对比

当我接到一个需求的时候,其实脑子里面闪现过许多实现需求的方法,到底用哪一种方法,取决于很多因素:代码复杂度,可扩展性,稳定性,代码执行效率等等。

今天Fabric主要从性能方面来分析两种实现方式的优劣,下面是一张三种方式动态计算高度(我们把Xib+约束动态计算单元格高度当作第三种自适应方法),加载UITableView所需时间的柱状图:

从自适应单元格高度说起-浅谈如何提高UITableView的加载效率当然,耗时的多少还和文本的大小有关系,Fabric为了凸显3种方法的效率差别故意把文本内容设置的很长。

正如大家看到的,代码动态计算高度的耗时要远远地高于后两者,效率非常低下,当我们把cell总数设置为1000,甚至10000的时候,可以很明显的感受到加载缓慢,严重的伤害了用户体验。

性能差别分析

大家可能会惊讶,短短几行代码,为什么耗时的差距可以高达上万倍呢?!

原因在于:当使用代码动态计算高度时,UITableView会首先执行一遍

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
    
    return [RBAutoSizeTableViewCell calculateTitleWidth:self.titles[indexPath.row]] + 20.0*2;
}

方法,当有1000个cell的时候,UITableview就会首先执行1000次计算高度的方法,然后再去执行- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath获取cell,获取cell之后,又会执行一次- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath方法,来获取当前cell的高度。这样一来,肯定要耗费非常长的时间。

反观第二种方法,UITableView只会预加载一个UITableView contentSize的内容,也就是说,无论有多少cell,UITableView会先加载一屏内容,再预计算第二屏的高度,不会有更多的计算操作。这种预加载逻辑,保障了UITableView既不会卡顿,也不会消耗更多的资源。

另外看一下Xib+约束的执行效率,并不比纯代码要低,可能有读者会有疑问:

  • 1、UITableView上一次性创建的Xib文件不多所以看不出性能差别。

  • 2、Xib文件上只有一个UILabel,太简单了,所以看不出Xib文件的耗时。

所以Fabric把行高设置成5px,让UITableView一次性多生成一些cell;尽量多拖拽一些控件到Xib上,增加Xib文件的复杂度,执行结果显示:纯代码构建cell和用Xib获取cell没有明显的性能区别。因此,Xib文件的执行效率是很高的,并不像我起先设想的那样,读取XML文件会很耗时。


总结

通过动态加载单元格的性能实验,我们知道了UITableView加载缓慢的原因:重复执行了大量的耗时操作,因此Fabric总结了以下几点提高UITableView加载效率的方法:

  • 1、不要在UITableViewDataSource的代理方法中加入过多的耗时方法,比如说计算宽高或者加载数据。
  • 2、尽量复用自定义的UITableViewCell,而不是定义非常多个UITableViewCell,毕竟从缓存池里获取cell要比重新创建cell要来的快。
  • 3、对于需要反复使用的数据建议加入缓存,比如说我们要重复获取一张名字为"Fabric"的图片,那么我们可以用如下代码:
- (UIImage *)getCellImage:(NSString *)imageName{
   
   if(!imageName) return nil;
   UIImage *img = [self.imageDict objectForKey:imageName];
   if(!img){
       img = [UIImage imageNamed:imageName];
       [self.imageDict setValue:img forKey:imageName];
   }
   return img;
}

当然,无论是第三方SDWebImage还是系统方法+ (nullable UIImage *)imageNamed:(NSString *)name,都已经帮我们将图片存储在磁盘上了,不需要我们再次去做缓存了,Fabric只是用图片缓存举个例子而已。

  • 4、尽量不要在- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath方法中,获取到cell之后再去addSubView,如果这样做的话,cell每一次出现在用户界面上就add一次subView,那么用户来回滑动几次UITableView,就会发现界面卡顿,滑动明显变慢,甚至滑不动了。
  • 5、多用hidden属性去隐藏对用户不可见的控件,而不是通过设置alpha为0,或者设置控件宽高为0的方式来隐藏控件,因为当控件的hidden属性为YES的时候,系统会自动优化控件内存,减少设备的资源消耗。

Fabric能想到的优化UITableView加载效率的方法就只有以上这么多了,欢迎大家在文章下方留言一起探讨,也可以加我的微信justlikeitRobert和我讨论,喜欢这篇文章请点赞,谢谢大家的关注与支持。

相关推荐