锦妖和她的小伙伴们 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; }
先将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所需时间的柱状图:
当然,耗时的多少还和文本的大小有关系,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加载效率的方法:
- (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的时候,系统会自动优化控件内存,减少设备的资源消耗。Fabric能想到的优化UITableView加载效率的方法就只有以上这么多了,欢迎大家在文章下方留言一起探讨,也可以加我的微信justlikeitRobert和我讨论,喜欢这篇文章请点赞,谢谢大家的关注与支持。