大家基本上都做过这样的需求: 在 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 所需时间的柱状图:
当然, 耗时的多少还和文本的大小有关系, 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 + 约束的执行效率, 并不比纯代码要低, 可能有读者会有疑问:
1UITableView 上一次性创建的 Xib 文件不多所以看不出性能差别
2Xib 文件上只有一个 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 的时候, 系统会自动优化控件内存, 减少设备的资源消耗
来源: https://juejin.im/post/5a97b9436fb9a028c9797a19