UITableViewCell计算行高的几种方式

原文 : 与佳期的个人博客(gonghonglou.com)

当我还是小白时(比现在更白的时候)对于 UITableViewCell 的行高问题还是比较头疼的,当然这个算是 iOS 开发中相当基础的内容了,但是当时的我就想过,将 UITableViewCell 的行高问题解决了一定写一篇总结博客,所以就有了这篇博客。尽管拖了这。么。久。。。

iOS 的界面中,UITableView 应该是用的最多的控件之一了吧:微信列表、聊天记录、朋友圈、微博 time line。。。哪哪离不开 UITableView。而 UITableView 则是由 UITableViewCell 组成的,这些 cell 有的行高是固定的,大部分则需要根据内容反计算行高来展示。本篇博客则来介绍 UITableViewCell 计算行高的几种方式。

固定行高

有相当一部分 UITableView 的行高是固定的,这种 cell 在代码书写和代码性能上相比而言就简单了许多,有以下两种方式设定行高:

统一设定

self.tableView.rowHeight = 44; // 系统自带的 cell 的行高大概就是 44
  • 优点:这种方式最为简单。
  • 缺点:相对的对 tableView 的可控性也最弱,它会将 tableView 所有的 cell 高度统一设置为 44。

通过代理设定

通过实现 UITableViewDelegate 方法,同样可以控制 tableView 的行高。

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    // 1.
    // return 44;
    
    // 2.
    // if (tableView == self.tableView1) {
    //  return 44;
    // } else {
    //  return 88;
    // }
    
    // 3.
    if (indexPath.row == 1) {
        return 44;
    } else {
        return 88;
    }
}
  • 优点:采用这种实现代理的方法可以对不同的 tableView 、tableView 中不同的 section、row,进行判断分别设置,可控性更强。
  • 缺点:因为在每次展示 cell 时都会调用了一次该代理方法,所以较于第一种方法一些性能损耗。

这个缺点在苹果介绍 rowHeight 属性的文档里也有指明:

There are performance implications to using tableView:heightForRowAtIndexPath: instead of rowHeight. Every time a table view is displayed, it calls tableView:heightForRowAtIndexPath: on the delegate for each of its rows, which can result in a significant performance problem with table views having a large number of rows (approximately 1000 or more).

所以,如果你的 tableView 的行高是统一的、固定的,那么最好采用第一种方法,直接设置 rowHeight。

不定行高

固定行高的 tableView 已经算是小儿科了,但是也属于 UITableViewCell 行高的范畴,所以还是简单提了一下。
那么,当 tableView 的行高不固定时,有以下几种计算方式。

估算行高

其实,UITableView 的 rowHeight 可以设置为 UITableViewAutomaticDimension,顾名思义,cell 可以设置为自动计算行高。然而,仅仅将 rowHeight 设置为 UITableViewAutomaticDimension 对自计算行高是不起效的,这时候就需要另一个属性:estimatedRowHeight

iOS8 苹果推出了 self-sizing 的概念。UITableView 在 iOS7 就增加了一个属性: estimatedRowHeight ,苹果是这样描述的:

Providing a nonnegative estimate of the height of rows can improve the performance of loading the table view. If the table contains variable height rows, it might be expensive to calculate all their heights when the table loads. Using estimation allows you to defer some of the cost of geometry calculation from load time to scrolling time.
When you create a self-sizing table view cell, you need to set this property and use constraints to define the cell’s size.
The default value is 0, which means there is no estimate.

你可以使用这个属性来给 cell 估算行高,但是默认值为 0,不会进行估算。
所以如果想开启估算行高的话,必须设置 estimatedRowHeight,如:

self.tableView.estimatedRowHeight = 60;
self.tableView.rowHeight = UITableViewAutomaticDimension;

将 estimatedRowHeight 设置为一个大概的估计行高值即可,没有严格的限制。比如你的 cell 高度大概在 50 到 100 之间,那么你可以将 estimatedRowHeight 设置为 75;
rowHeight 的默认值为 UITableViewAutomaticDimension,所以第二行可以省略。

然后就是对你的 cell 进行布局设置,这里以 Masonry 为例(AutoLayout 的话,这个框架大家应该都在用吧?~)
伪代码如下:

- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if (self) {
        __weak __typeof(self)weakSelf = self;
        
        UILabel *label1 = [UILabel new];
        [self.contentView addSubview:label1];
        [label1 mas_makeConstraints:^(MASConstraintMaker *make) {
            __strong __typeof(weakSelf)strongSelf = weakSelf;
            make.top.equalTo(strongSelf.contentView).with.offset(10);
            make.left.equalTo(strongSelf.contentView).with.offset(10);
            make.right.equalTo(strongSelf.contentView).with.offset(-10);
            make.height.mas_equalTo(22); // ①
        }];
        
        UILabel *label2 = [UILabel new];
        [label2 setNumberOfLines:0];
        [self.contentView addSubview:label2];
        [label2 mas_makeConstraints:^(MASConstraintMaker *make) {
            __strong __typeof(weakSelf)strongSelf = weakSelf;
            make.top.equalTo(label1.mas_bottom).with.offset(10);
            make.left.equalTo(label1);
            make.right.equalTo(label1);
            make.bottom.equalTo(strongSelf.contentView.mas_bottom).with.offset(-10); // ②
        }];
    }
    return self;
}

注释①:该条约束可以注释掉,然后将 label1 设置为 [label1 setNumberOfLines:0]; 这样 label1 就可以根据内容自动计算高度。
注释②:必须设置该条约束。为了确定 cell 的高度,最接近 cell bottom 的控件需要设置一个距离 cell bottom 的约束。

布局结果大概是这样的效果:
自动布局结果
  • 优点:代码书写简单,页面布局快捷。在 cell 的 initWithStyle: 方法里就已经新建各个控件并将位置设置好,并且不需要单独计算 cell 的高度。
  • 缺点:效率低,稍微复杂些的页面就能感觉到 tableView 滑动时的掉帧。

代理计算行高

首先来执行一波伪代码,用打印来观察各个代理方法的执行顺序:

#pragma mark - UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    NSLog(@"---方法 : numberOfRowsInSection: ---section : %ld", (long)section);
    
    return 10;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    NSLog(@"---方法 : cellForRowAtIndexPath: ---row : %ld", (long)indexPath.row);
    
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
    return cell;
}

#pragma mark - UITableViewDelegate
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    NSLog(@"---方法 : heightForRowAtIndexPath: ---row : %ld", (long)indexPath.row);

    return cellHight;
}

控制台打印结果如下:
代码执行顺序

①:首先执行 numberOfRowsInSection: 方法,返回 cell 个数为 10。
②:其次执行的就是 heightForRowAtIndexPath: 方法,如上图,此时执行该方法会将所有 cell 的高度全部返回。
③④⑤:这时候就开始执行 cellForRowAtIndexPath: 方法,因为当前页面只能布局 3 条 cell,所以该方法会被执行三次。并且,执行一次 cellForRowAtIndexPath: 方法紧接着就会执行一次 heightForRowAtIndexPath: 方法返回 cell 高度。

因此,当我们从网络或者本地缓存中获取到所需数据( array )后,可以直接执行代码:

[self.tableView reloadData];

然后就会调用 cellForRowAtIndexPath: 方法和 heightForRowAtIndexPath: 方法。

cell 的 initWithStyle: 方法:

- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if (self) {        
        UILabel *label1 = [UILabel new];
        [self.contentView addSubview:label1];
        
        UILabel *label2 = [UILabel new];
        [label2 setNumberOfLines:0];
        [self.contentView addSubview:label2];
    }
    return self;
}

我们可以在 cellForRowAtIndexPath: 方法进行 cell 布局,如:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
    if (!cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
    }

    // 内容
    cell.label1.text = @"...";
    cell.label2.text = @"...";
    
    // 布局
    CGFloat viewWidth = [UIScreen mainScreen].bounds.size.width;
    cell.label1.fream = CGRectMake(10, 10, viewWidth-20, 22);
    
    CGFloat label2Height = [text boundingRectWithSize:CGSizeMake(viewWidth-20, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:font} context:nil].size.height;
    cell.label2.fream = CGRectMake(10, 10, viewWidth-20, label2Height);
    return cell;
}

布局结果大概是这样的效果:
手动布局结果

然后在 heightForRowAtIndexPath: 方法里根据 array 数据计算 cell 的高度,如:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    CGFloat viewWidth = [UIScreen mainScreen].bounds.size.width;
    CGFloat label2Height = [text boundingRectWithSize:CGSizeMake(viewWidth-20, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:font} context:nil].size.height;
    return 10 + 22 + 10 + label2Height + 10;
}
  • 优点:通过 feram 的方式能够提高布局效率
  • 缺点:因为每次 cell 将要出现时都去执行一次 heightForRowAtIndexPath: 所以效率还是不够高,而且行高没有得到缓存,造成大量不必要的计算上的浪费。所以就有了下边这种方法。

提前计算行高

这种方式是出自 MVVM 的产物,因为最近都在使用 MVVM 框架,所有的计算都放到了 viewModel 里,所以在 viewModel 从网络或者本地缓存拿到数据后接着就会处理,将原始数据处理封装成 cell 的 VO(view object) 类对象,该 VO 类里包含着 cell 所需要的展示内容和尺寸,如:

@interface TableViewCellVO : NSObject
// 数据
@property (nonatomic, copy) NSString *label1Text;
@property (nonatomic, copy) NSString *label2Text;
// 尺寸
@property (nonatomic, assign) CGRect label1Fream;
@property (nonatomic, assign) CGRect label2Fream;
// 行高
@property (nonatomic, assign) CGFloat cellHeight;
@end

然后将处理好的 cellVOArray 传给 UITableView 的数据源和代理,在 cellForRowAtIndexPath: 方法直接赋值:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
    if (!cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
    }
    TableViewCellVO *cellVO = cellVOArray[indexPath.row];
    // 数据
    cell.label1.text = cellVO.label1Text;
    cell.label2.text = cellVO.label2Text;
    // 布局
    cell.label1.fream = cellVO.label1Fream;
    cell.label2.fream = cellVO.label2Fream;
    return cell;
}

heightForRowAtIndexPath: 方法里直接返回行高:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    TableViewCellVO *cellVO = cellVOArray[indexPath.row];
    return cellVO.cellHeight
}
  • 优点:行高与 cell 内的各控件尺寸都提前计算好,在执行 cellForRowAtIndexPath: 方法和 heightForRowAtIndexPath: 方法时比较快,且行高得到缓存,避免了冗余的计算。
  • 缺点:在从网络或者本地缓存拿到数据之后,执行 [self.tableView reloadData]; 之前需要花费时间处理数据及计算 fream。这里需要谨慎处理数据,可以采用多线程等技术缩短数据处理的时间。

因为在数据处理上所花费的时间要远小于页面滚动时所消耗的时间。凡是涉及页面的操作都是相当耗费时间的,相比而言对于 cpu 在处理数据上的时间就可以忽略不计了,毕竟我们所要处理的数据都不会太大,如果数据过多的话可以做分次获取处理(下拉刷新操作),所以这里的缺点相对于上一种方法还是可以忍受的。

XIB 方式处理 UITableViewCell 的行高问题

对于纯 Code 和 SB 方式的页面布局问题业界已经相爱相杀了很久,各有各的道理。我的观点是:哪种布局方式适合自己就好了,看个人喜好。相比于 SB 的“所见即所得”,快速布局等,我个人更喜欢纯 Code 的方式,对代码的可控性更强(亦不排除我个人是比较喜欢 Coding 的因素)。
不过,如果你是偏好 SB 的布局方式,采用 XIB 方式处理 UITableViewCell,那么你可以看看sunnyxxUITableView-FDTemplateLayoutCell(点击可查看GitHub)这种解决方案。
而且,本文的前一部分也参考了 sunnyxx 的这篇博客 优化UITableViewCell高度计算的那些事
,推荐阅读。

后记

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容