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

你们基本上都作过这样的需求:在UITableView上展现文本,且文本内容长短不一,每一行单元格都要动态计算高度,使得单元格能够恰好容纳下须要展现的文字。为了方便讲解,咱们把文本框设定成一个距离cell上下左右均有20px间距的UILabel,须要单元格动态调整高度,使得文本框恰好能够展现出全部的文本内容。git

实现方案

需求自己并非很是复杂,实现这个需求基本上能够采用两种方法:github

一、代码动态计算高度数组

二、利用iOS8中UITableView的estimatedRowHeight新特性经过约束计算高度缓存

咱们先来看一下两种方案的实现方式:微信

代码动态计算高度

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

+ (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设置为预估高度的模式:ui

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+约束的执行效率,并不比纯代码要低,可能有读者会有疑问:

  • 一、UITableView上一次性建立的Xib文件很少因此看不出性能差异。

  • 二、Xib文件上只有一个UILabel,太简单了,因此看不出Xib文件的耗时。

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


总结

经过动态加载单元格的性能实验,咱们知道了UITableView加载缓慢的缘由:重复执行了大量的耗时操做,所以Fabric总结了如下几点提升UITableView加载效率的方法:

  • 一、不要在UITableViewDataSource的代理方法中加入过多的耗时方法,好比说计算宽高或者加载数据。
  • 二、尽可能复用自定义的UITableViewCell,而不是定义很是多个UITableViewCell,毕竟从缓存池里获取cell要比从新建立cell要来的快。
  • 三、对于须要反复使用的数据建议加入缓存,好比说咱们要重复获取一张名字为"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只是用图片缓存举个例子而已。

  • 四、尽可能不要在- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath方法中,获取到cell以后再去addSubView,若是这样作的话,cell每一次出如今用户界面上就add一次subView,那么用户来回滑动几回UITableView,就会发现界面卡顿,滑动明显变慢,甚至滑不动了。
  • 五、多用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和我讨论,喜欢这篇文章请点赞,谢谢你们的关注与支持。

相关文章
相关标签/搜索