你们基本上都作过这样的需求:在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;
}
复制代码
先将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所需时间的柱状图:
正如你们看到的,代码动态计算高度的耗时要远远地高于后二者,效率很是低下,当咱们把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加载效率的方法:
- (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的时候,系统会自动优化控件内存,减小设备的资源消耗。首先,感谢两位读者Asuray和ControlM给Fabric的宝贵留言。他们一针见血的指出了代码计算高度自适应UITableView效率低下的缘由:在UITableViewDataSource的代理方法中,执行了过多的冗余的计算UILabel高度的操做。
Fabric的方法一是一个不恰当的加载UITableView的思路,旨在让读者看到UITableView加载效率低下的缘由。下面咱们来设想一下如何优化,结合上文总结的五点提升UITableView加载效率的方法,Fabric想出了如下三点改进方法:
- (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];
}];
}
复制代码
- (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;
}
复制代码
- (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加载耗时的柱状图:
Fabric能想到的优化UITableView加载效率的方法就只有以上这么多了,欢迎你们在文章下方留言一块儿探讨,也能够加个人微信justlikeitRobert和我讨论,喜欢这篇文章请点赞,谢谢你们的关注与支持。