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

大家基本上都做过这样的需求:在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;
复制代码

只需要两行代码,我们就完成了动态高度的估算工作,非常的简洁明了。

这里我用了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所需时间的柱状图:

当然,耗时的多少还和文本的大小有关系,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的时候,系统会自动优化控件内存,减少设备的资源消耗。

后续 - 提高方法一(代码计算高度)的UITableView的加载效率

首先,感谢两位读者Asuray和ControlM给Fabric的宝贵留言。他们一针见血的指出了代码计算高度自适应UITableView效率低下的原因:在UITableViewDataSource的代理方法中,执行了过多的冗余的计算UILabel高度的操作

Fabric的方法一是一个不恰当的加载UITableView的思路,旨在让读者看到UITableView加载效率低下的原因。下面我们来设想一下如何优化,结合上文总结的五点提高UITableView加载效率的方法,Fabric想出了以下三点改进方法:

  • 1.加入缓存机制,即把title的内容写入Model,在Model中计算出UILabel的高度,避免UITableView每次获取高度都要计算一遍高度,也避免了在获取到Cell的时候计算UILabel高度,代码如下:
- (void)convertDataToModel{
    
    [self.titles enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        RBTitleModel *titleModel = [[RBTitleModel alloc] init];
        titleModel.title = obj;
        titleModel.titleLabelHeight = 0.0f;
        [self.titles replaceObjectAtIndex:idx withObject:titleModel];
    }];
}
复制代码
  • 2.在- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath方法中不再执行计算UILabel高度的方法,而是给出一个预设的高度,代码如下:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
    
    RBTitleModel *model = [self.titles objectAtIndex:indexPath.row];
    return model.titleLabelHeight + 20.0*2;
}
复制代码
  • 3.单纯的把文字高度计算放入Model中效率还是极其低下的,因为在reloadData之前,需要执行所有的Model的计算高度代码,假设数据源是有1000000个元素的数组,那么在转换model之前,就需要计算1000000次高度。所以最好的方法是在- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath方法中计算UILabel的高度,代码如下:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    
    static NSString *autoSizeTableViewCellID = @"RBAutoSizeTableViewCell";
    RBAutoSizeTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:autoSizeTableViewCellID];
    if(!cell){
        cell = [[RBAutoSizeTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:autoSizeTableViewCellID];
    }
    //动态计算当前cell的高度
    RBTitleModel *titleModel = [self.titles objectAtIndex:indexPath.row];
    [titleModel calculateTitleWidth];
    
    [cell buildData:self.titles[indexPath.row]];
    return cell;
}
复制代码

在计算高度时,Fabric采用了缓存机制,如果titleModel.titleHeight的数值不为0,说明已经计算过高度不需要重复计算,代码如下:

- (void)calculateTitleWidth{
    //有缓存则不需要重复计算
    if(self.titleLabelHeight > 0) return;
    
    CGFloat stringWidth = 0;
    CGSize size = CGSizeMake(kRBScreenWidth - 20.0f*2, MAXFLOAT);
    
    if (self.title.length > 0) {
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000
        stringWidth = [self.title
                       boundingRectWithSize:size
                       options:NSStringDrawingUsesLineFragmentOrigin
                       attributes:@{NSFontAttributeName:kRBTextFont}
                       context:nil].size.height;
#else
        //iOS7.0以下方法
        stringWidth = [self.title sizeWithFont:kRBTextFont
                        constrainedToSize:size
                            lineBreakMode:NSLineBreakByCharWrapping].height;
#endif
    }
    self.titleLabelHeight = stringWidth;
}
复制代码

经过改进之后,UITableView的执行效率明显变高了,下图是改进之后的两种方式的UITableView加载耗时的柱状图:

虽然代码计算高度的效率还是最低的,但是相比之前要好了很多。感兴趣的同学可以去我的GitHub上下载 Demo,阅读源码,也可以自己动手实现一下。大家有更好的优化UITableView加载效率的方法,也可以直接在Demo中修改,然后push给Fabric,大家共同进步,一起提高技术水平。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值