美文网首页iOS开发资料收集区ios开发学习iOS开发
配合Masonry实现TableViewCell的高度自适应,以

配合Masonry实现TableViewCell的高度自适应,以

作者: 翻炒吧蛋滚饭 | 来源:发表于2017-01-11 18:19 被阅读5350次

前言

对于TableViewCell的高度自适应,很多初次接触的同学,还是很头痛的。就算已经有些开发经验的同学,处理起来也可能用错了方法。但其实系统已经提供了很方便的处理方法,我们这里就系统的高度计算做一个讲解。然后主要要讲的,是我在实际开发中(我们App加入了直播功能,直播中要处理大量的聊天消息)用到的方法,也是在性能上优化了很多的方法,将计算好的高度缓存下来,在大量数据(几百、几千条数据)进行刷新、插入数据、删除数据等操作的时候也能保证性能、流畅性,而相比于其他高度缓存方案,这种方式的高度缓存,更方便管理。以下高度都结合Masonry来完成(毕竟手写Autolayout还是Masonry比较方便),使用XIB的同学,也可以直接拖约束。

场景模拟

我们写个Demo,来模拟下直播聊天室中情况,众所周知,直播聊天室中的消息量是巨大的,而且刷新特别快,在刷新聊天列表的时候,最耗费性能的就是UITableView的两个代理方法,一个heightForRowAtIndexPath,一个cellForRowAtIndexPath。无论是刷新还是新增、删除,都会反复触发这两个方法,而对于聊天室,如果从后面追加数据,假设你原来有1000条数据,即使你从后面insert一个cell,那也会调用1000次HeightForRow,如果你在计算高度的时候,使用了很复杂的计算方式,就很影响性能了。
  首先新建个项目,然后在项目中加入Masonry,再然后加入一个显示当前屏幕FPS的label进来,提取自YYKit,YYFPSLabel。这样就能大致了解性能如何了。然后我们在ViewController.m中加入这个控件:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    YYFPSLabel *fpsLabel = [[YYFPSLabel alloc] initWithFrame:CGRectMake(0, 20, 60, 20)];
    [self.view addSubview:fpsLabel];
}

运行后我们的Demo顶部就会显示FPS了:


Paste_Image.png

  然后我们先建一个Model,和一个Cell,Model代表我们从服务器请求的数据模型,Cell就是我们要用到的展示内容的Cell。为了让Cell更符合实际项目的需求,我们让cell显示多一些的内容,来一个拼接的属性字符串吧。
  新建个Model:


Paste_Image.png
  模拟聊天中的消息展示,我们给Model两个属性,一个姓名,一个发言内容:
// 姓名
@property (nonatomic, copy) NSString *name;
// 发言内容
@property (nonatomic, copy) NSString *message;

我们再新建一个Cell,在Cell中将内容展示出来:


Paste_Image.png

  我们的Cell中只有一个Label,用于展示“姓名:发言内容”这样的内容,注意这里布局,采用自动布局,Cell的ContentView由Label中的内容撑开:

@interface MessageCell ()

@property (nonatomic, strong) UILabel *messsageLabel;

@end

@implementation MessageCell

- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    if (self == [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
        // 创建UI
        [self createUI];
    }
    
    return self;
}

- (void)createUI {
    /** 发言 */
    self.messsageLabel = [[UILabel alloc] init];
    self.messsageLabel.numberOfLines = 0;
    [self.contentView addSubview:self.messsageLabel];
    [self.messsageLabel mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.mas_equalTo(8);
        make.left.mas_equalTo(10);
        make.right.mas_equalTo(-10);
        make.bottom.mas_equalTo(-8);
    }];
}

- (void)setMessage:(CellModel *)message {
    // 创建一个可变属性字符串
    NSMutableAttributedString *finalStr = [[NSMutableAttributedString alloc] init];
    
    // 创建姓名
    NSAttributedString *nameStr = [[NSAttributedString alloc] initWithString:message.name attributes:@{NSFontAttributeName: [UIFont systemFontOfSize:16], NSForegroundColorAttributeName: [UIColor redColor]}];
    
    // 创建发言内容
    NSAttributedString *messageStr = [[NSAttributedString alloc] initWithString:message.message attributes:@{NSFontAttributeName: [UIFont systemFontOfSize:16], NSForegroundColorAttributeName: [UIColor blackColor]}];
    
    // 拼接上两个字符串
    [finalStr appendAttributedString:nameStr];
    [finalStr appendAttributedString:messageStr];
    self.messsageLabel.attributedText = finalStr;
}
@end

这里我们需要注意的是,Label要高度自适应的撑开Cell的ContentView的高度。然后我们去ViewController中添加一个用于展示这些内容的TableView,在viewDidLoad方法的结尾,我们添加一个按钮,该按钮模拟聊天室中接收到了新消息,并滚动到TableView的最底部。具体代码如下:

@interface ViewController () <UITableViewDelegate, UITableViewDataSource>

@property (nonatomic, strong) UITableView *tableView;
@property (nonatomic, strong) NSMutableArray *dataArr;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    YYFPSLabel *fpsLabel = [[YYFPSLabel alloc] initWithFrame:CGRectMake(0, 20, 60, 20)];
    [self.view addSubview:fpsLabel];
    
    // 创建TableView
    self.tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 100, self.view.frame.size.width, self.view.frame.size.height-100) style:0];
    self.tableView.dataSource = self;
    self.tableView.delegate = self;
    [self.view addSubview:self.tableView];
    // 注册cell
    [self.tableView registerClass:[MessageCell class] forCellReuseIdentifier:@"MessageCell"];
    
    // 模拟一些数据源
    NSArray *nameArr = @[@"张三:",
                         @"李四:",
                         @"王五:",
                         @"陈六:",
                         @"吴老二:"];
    NSArray *messageArr = @[@"ash快点回家爱是妒忌哈市党和国家按时到岗哈时代光华撒国会大厦国会大厦国会大厦更好的噶山东黄金撒旦哈安师大噶是个混蛋撒",
                            @"傲世江湖点撒恭候大驾水草玛瑙现在才明白你个坏蛋擦边沙尘暴你先走吧出现在",
                            @"撒点花噶闪光灯",
                            @"按时间大公司大概好久撒大概好久撒党和国家按时到岗哈师大就萨达数据库化打算几点撒谎就看电视骄傲的撒金葵花打暑假工大撒比的撒谎讲大话手机巴士差距啊市场报价啊山东黄金as擦伤擦啊as擦肩时擦市场报价按时VC阿擦把持啊三重才撒啊双层巴士吃按时吃啊双层巴士擦报啥错",
                            @"as大帅哥大孤山街道安师大好噶时间过得撒黄金国度"];
    // 向数据源中随机放入500个Model
    self.dataArr = [[NSMutableArray alloc] init];
    for (int i=0; i<500; i++) {
        CellModel *model = [[CellModel alloc] init];
        model.name = nameArr[arc4random()%nameArr.count];
        model.message = messageArr[arc4random()%messageArr.count];
        [self.dataArr addObject:model];
    }
    
    // 我们再创建一个按钮,点击可从后面追加一些数据进来
    UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(0, 40, 100, 60)];
    button.backgroundColor = [UIColor redColor];
    [self.view addSubview:button];
    [button addTarget:self action:@selector(addData) forControlEvents:UIControlEventTouchUpInside];
}

- (void)addData {
    // 添加一个Model,在追加到Tableview中
    CellModel *model = [[CellModel alloc] init];
    model.name = @"皮皮:";
    model.message = @"安师大公司的嘎斯大时代安师大嘎斯高大上撒旦嘎嘎就是打闪光灯";
    [self.dataArr addObject:model];
    
    // 插入到tableView中
    [self.tableView insertRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:self.dataArr.count-1 inSection:0]] withRowAnimation:UITableViewRowAnimationNone];
    // 再滚动到最底部
    [self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:self.dataArr.count-1 inSection:0] atScrollPosition:UITableViewScrollPositionBottom animated:YES];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.dataArr.count;
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return 44;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    MessageCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MessageCell" forIndexPath:indexPath];
    [cell setMessage:self.dataArr[indexPath.row]];
    return cell;
}

@end

效果如下,这里我们固定Cell高度为44了,所以全程怎么滚动,FPS都是60:


9BAB5AB9072DACA29A7084C28B42DDA9.png

动态高度一:系统自带支持

那好了,上面的固定高度测试完了,我们来测试下适配Cell高度的方法。首先采用系统的动态高度方法。
  我们需要做两件事:第一:指定TableView的高度为自适应:

// 必须设置预估高度才能生效
self.tableView.estimatedRowHeight = 100;
self.tableView.rowHeight = UITableViewAutomaticDimension;

第二:将TableView的行高代理方法注释掉,也就是下面这个方法:

//- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
//    return 44;
//}

这时再运行,你会发现,Cell的高度已经自动适配,滚动中也特别流畅,保持60帧:


Paste_Image.png

  但如果点击我们的红色按钮,就卡爆了,而且会有一个刷新的白屏:


Paste_Image.png
  实测系统的这个方法,只适用于iOS8及以上,且在数据量超大的时候,进行插入和删除,都是很不流畅的,不建议采用。当然这种方法针对一些常用场景,比如新闻列表、商品列表什么的,数据量没那么大且不涉及到新增、删除数据的时候,这种方法,还是蛮不错的,写起来很简便。

动态高度二:自己计算高度

我们将上面的方法撤回,试验下自己计算Cell高度,性能如何。

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    // 创建一个可变属性字符串
    NSMutableAttributedString *finalStr = [[NSMutableAttributedString alloc] init];
    
    // 取出Model
    CellModel *message = self.dataArr[indexPath.row];
    
    // 创建姓名
    NSAttributedString *nameStr = [[NSAttributedString alloc] initWithString:message.name attributes:@{NSFontAttributeName: [UIFont systemFontOfSize:16], NSForegroundColorAttributeName: [UIColor redColor]}];
    
    // 创建发言内容
    NSAttributedString *messageStr = [[NSAttributedString alloc] initWithString:message.message attributes:@{NSFontAttributeName: [UIFont systemFontOfSize:16], NSForegroundColorAttributeName: [UIColor blackColor]}];
    
    // 拼接上两个字符串
    [finalStr appendAttributedString:nameStr];
    [finalStr appendAttributedString:messageStr];
    
    // 计算高度
    CGSize size = [finalStr boundingRectWithSize:CGSizeMake(self.view.frame.size.width-20, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin context:nil].size;
    return ceil(size.height);
}

这种方式,在滚动列表的时候,还是60帧流畅的,点击红色按钮后,会降到47帧,并持续一小段时间,所以这段时间中,你如果是在聊天室中播放弹幕,或者进行点赞动画的处理的时候,这些内容都会卡住,直到这段时间过去,当然相比于系统的方法,性能还是稍好一点的:


Paste_Image.png

动态高度三:Autolayout计算高度

有人可能觉得,上面计算高度太麻烦了,不就是把Cell中setMessage拿出来再写一遍嘛,同样的代码不要写两次,那我们换种方式来写。这里我们先给ViewController这个Controller加一个属性,下面的这个Cell,承担了计算Cell高度的工作:

@property (nonatomic, strong) MessageCell *tempCell;

在viewDidLoad中初始化:

self.tempCell = [[MessageCell alloc] initWithStyle:0 reuseIdentifier:@"MessageCell"];

然后我们给Cell加个方法,这里需要注意的是,我们要对最终算出来的高度加1,这个1是Cell的分割线的高度,当前如果你隐藏了分割线,就不需要加这个1了:

// 根绝数据计算cell的高度
- (CGFloat)heightForModel:(CellModel *)message {
    [self setMessage:message];
    [self layoutIfNeeded];
    
    CGFloat cellHeight = [self.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height+1;
    
    return cellHeight;
}

还要指定Cell中的Label的最大宽度,保证在适配Label的时候,不会超出这个宽度:

self.messsageLabel.preferredMaxLayoutWidth = [UIScreen mainScreen].bounds.size.width-20;

最后我们来获取Cell的高度:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return [self.tempCell heightForModel:self.dataArr[indexPath.row]];
}

运行后,跟方案二的效果一样,甚至性能还不如方案2,这种方案的好处就是不需要计算高度,高度由系统Autolayout计算好。最后我们引入方法四,再优化一些性能。

动态高度四:缓存高度

性能的损耗大部分都在heightForRowAtIndexPath这个方法上,我们有500条数据,当我们点击红色按钮后,会刷新tableView,这时就会调用501(加上我们新插入的数据)次heightForRowAtIndexPath方法,所以每个Cell的高度都会重新算一次,这样性能就大打折扣,那我们想办法不让他算呗,那就把计算好的高度缓存下来吧。所以我们在Model中加入一个属性,用于保存Model所对应的Cell的高度。所以最后我们Model中的属性有这几个:

@interface CellModel : NSObject

// 姓名
@property (nonatomic, copy) NSString *name;
// 发言内容
@property (nonatomic, copy) NSString *message;
// 该Model对应的Cell高度
@property (nonatomic, assign) CGFloat cellHeight;

@end

然后我们来到TableView的Cell高度的代理方法中,如果当前Model的cellHeight为0,说明这个Cell没有缓存过高度,则计算Cell的高度,并把这个高度记录在Model中,这样下次再获取这个Cell的高度,就可以直接去Model中获取,而不用重新计算了:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    CellModel *model = self.dataArr[indexPath.row];
    if (model.cellHeight == 0) {
        CGFloat cellHeight = [self.tempCell heightForModel:self.dataArr[indexPath.row]];
        
        // 缓存给model
        model.cellHeight = cellHeight;
        
        return cellHeight;
    } else {
        return model.cellHeight;
    }
}

这样就实现了高度缓存和Model、Cell都对应的优化,我们无需手动管理高度缓存,在添加和删除数据的时候,都是对Model在数据源中进行添加或删除。
  最后再运行,你会发现,红色按钮,怎么点,都是60帧满,偶尔会掉到59,那也只是极为短暂的一个时间,可以忽略不计,这样,聊天室的刷新性能,就可以完美的解决了。

以上所有测试都在iPhone6s上进行,如果其他盆友也对TableView的性能优化感兴趣,希望可以告知我其他型号手机的运行效果,或者如果有更高效的处理方法,都可以联系我,大家互相学习、共同进步。
  最后补上Demo:https://github.com/ZhaoheMHz/UITableVIewSelfSizing

相关文章

网友评论

  • 世界的一缕曙光:刚想起来,之前看文章说现在 iOS12 中对 Autolayout 的性能改进很明显:grin:
    翻炒吧蛋滚饭:@世界的一缕曙光 对:grin:现在iOS12用起来列表就很流畅
  • BetrayalPromise:适配高度我觉得最重要的是cell内容控件约束上下撑满就行了 剩下的就是填数据
    BetrayalPromise:@翻炒吧蛋滚饭 NSLayout这种解多元(不)等式的方式说实话 效率不高 所以我基本就不用了
    翻炒吧蛋滚饭:@BetrayalPromise 似的,所有操作都是基于这两个基本原则的
  • 跳跳虾:Demo 中没有做高度缓存吧?每次都会调用systemLayoutSizeFittingSize 方法计算高度,
    翻炒吧蛋滚饭:@lanmoyingsheng 对,辛苦回复哈哈
    lanmoyingsheng:if (model.cellHeight == 0) 为0时计算并缓存高度,不为0时就直接获取高度了
  • 请叫我小白同学:大哥,我按照你方案四来,做一个跟朋友圈差不多的动态的cell,不知道为图片一直显示不出来,高度很小~~~
  • 超_iOS:if (model.cellHeight == 0) {
    CGFloat cellHeight = [self.tempCell heightForModel:self.dataArr[indexPath.row]];

    // 缓存给model
    model.cellHeight = cellHeight;

    return cellHeight;
    }
    这一步还要吧model 替换数组里的吧..
    翻炒吧蛋滚饭:@_超 数组里的model被数组强引用了,你直接修改model就行。 具体的崩溃信息有吗?滚动到地步的?
    超_iOS:你是从数组里取的model,但改过的model没有替换数组里原来的啊。
    我现在在消息多的话滚动到底部时会崩溃,大神有建议么?
    翻炒吧蛋滚饭:@_超 indexpath可以加层判断,是否越界,model强引用不需要再添加
  • 赤小豆nil:楼主,问个问题哈。如果把分割线隐藏了,也就是tableView.separatorStyle = UITableViewCellSeparatorStyleNone;然后计算出来的高度,需要+1吗?
    赤小豆nil:@翻炒吧蛋滚饭 你在heightForRowAtIndexPath方法里,调用了heightForModel,heightForModel方法里调用了setMessage。感觉这步是多余的!!!其实在cellForRowAtIndexPath方法里调用setMessage的时候,就可以计算出高度,然后赋值给model。因为数据源的数组(dataArr)是浅拷贝,所以回到ViewController里数组也会变化,这时在heightForModel方法里面就可以不调用任何方法,直接去return “ model.cellHeight”!!!以上都我的愚见,有问题的话希望指出!:pray:
    赤小豆nil:@翻炒吧蛋滚饭 我没写下面两句代码
    self.tableView.estimatedRowHeight = 100;
    self.tableView.rowHeight = UITableViewAutomaticDimension;
    也一样适配了高度,没有出现任何问题。
    翻炒吧蛋滚饭:@赤小豆nil 这个还真不知道,但按理说分割线是cell的一部分吧,应该不用加1
  • 落枫猿:我想说:如果把这句代码( [self.tableView insertRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:self.dataArray.count-1 inSection:0]] withRowAnimation:UITableViewRowAnimationNone];)换成[self.tableView reloadData];使用您的第一种方案,性能一样也很好
    翻炒吧蛋滚饭:@落枫猿 主要还是在tableView滚动的时候很卡
  • 小丑_3bd1:老哥 方法中[self.tempCell heightForModel:self.dataArr[indexPath.row]];
    tempCell是在哪定义的呀
  • 小丑_3bd1:老哥,能否发个Demo,git进不去呀:sweat:
    翻炒吧蛋滚饭:@小丑_3bd1 哈哈哈:smile:
    小丑_3bd1:@翻炒吧蛋滚饭 没事老哥,我看着你的做出来了:grin:
    翻炒吧蛋滚饭:@小丑_3bd1 :sweat:我这本地没有啊
  • 绍清_shao:为什么要算高,正确的布局加系统算高方便快捷,
    你这样开发会不会很累啊,兄弟.
    心疼10秒......
    翻炒吧蛋滚饭:@绍清_shao 系统方法是快啊,但是性能没那么好
  • LByy:那如果只是显示一些网络上的图片,但是图片一开始你并不知道宽高比,你怎么对tab的cellHeight处理呢?从网络上先下载下来 获得image宽高 再处理嘛?期待你的回复 。
    翻炒吧蛋滚饭:@LByy 对的,当然我也没具体做过类似效果,不知道还有没有其他更好的方法
    LByy:你意思是说思路还是跟你这个一样是吧?关键就是什么时候拿到图片的宽高了
    翻炒吧蛋滚饭:@LByy 那只能等图片下载下来后更新下约束了,或者提前就从服务器获取图片的大小
  • 1ba5bc9bf644:厉害了大佬!!!帮大忙了!!!
    翻炒吧蛋滚饭:@抬头看见柠檬树 :grin:大佬不敢当,能帮上忙就行
  • First灬DKS:赞一个,如果另附一份儿demo,更好了!
    翻炒吧蛋滚饭:@First灬DKS 没事:yum:
    First灬DKS:@翻炒吧蛋滚饭 谢谢!
    翻炒吧蛋滚饭:在文章末尾补上了Demo:blush:
  • fero2004:
    // 根绝数据计算cell的高度
    - (CGFloat)heightForModel:(CellModel *)message {
    [self setMessage:message];
    [self layoutIfNeeded];

    CGFloat cellHeight = [self.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height+1;

    return cellHeight;
    }

    这个方法里计算高度其实和手动计算高度没什么差别嘛,就是系统帮你完成了.
    如果界面很复杂的话,还是要自己算
    翻炒吧蛋滚饭:@fero2004 不管界面多复杂,只要你cell是按照autolayout撑开contentView高度的话,都只需要写这些
  • f819d773bb62:强势插入!
    翻炒吧蛋滚饭:@我叫山鸡_ 哈哈哈
    我叫山鸡_:看完文章,觉得楼主是个有深度,有内涵的大神。结果我错了,看到评论,尤其这条,我觉得楼主应该菊花残了:sweat:
    翻炒吧蛋滚饭:@不懂的青春 〜( ̄△ ̄〜)

本文标题:配合Masonry实现TableViewCell的高度自适应,以

本文链接:https://www.haomeiwen.com/subject/tsbmvttx.html