`
iaiai
  • 浏览: 2203695 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

配合Masonry实现TableViewCell的高度自适应,以及更易管理的高度缓存

 
阅读更多
前言

  对于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了:

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

新建个Model:

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

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

我们的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:


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

那好了,上面的固定高度测试完了,我们来测试下适配Cell高度的方法。首先采用系统的动态高度方法。

我们需要做两件事:第一:指定TableView的高度为自适应:
// 必须设置预估高度才能生效
self.tableView.estimatedRowHeight = 100;
self.tableView.rowHeight = UITableViewAutomaticDimension;

第二:将TableView的行高代理方法注释掉,也就是下面这个方法:
//- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
//    return 44;
//}

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

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

实测系统的这个方法,只适用于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帧,并持续一小段时间,所以这段时间中,你如果是在聊天室中播放弹幕,或者进行点赞动画的处理的时候,这些内容都会卡住,直到这段时间过去,当然相比于系统的方法,性能还是稍好一点的:


动态高度三: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的性能优化感兴趣,希望可以告知我其他型号手机的运行效果,或者如果有更高效的处理方法,都可以联系我,大家互相学习、共同进步。
  • 大小: 6.6 KB
  • 大小: 6 KB
  • 大小: 6.4 KB
  • 大小: 227 KB
  • 大小: 404.8 KB
  • 大小: 21.7 KB
  • 大小: 459.5 KB
分享到:
评论

相关推荐

    tableViewCell 自适应高度

    以下是如何使用Masonry实现tableViewCell自适应高度的步骤: 1. **创建Cell**: 首先,你需要创建一个自定义的UITableViewCell子类,并在其中添加你需要展示的视图,例如UILabel或UIImageView。 2. **设置约束**: ...

    ios-根据Masonry自动布局Cell自适应高度.zip

    这个“ios-根据Masonry自动布局Cell自适应高度.zip”文件提供了一个使用Masonry库来实现这个功能的例子。Masonry是一款强大的自动布局库,它使得在Swift或Objective-C中进行自动布局变得更为简洁。 Masonry的核心...

    SDAutoLayout实现cell高度自适应

    SDAutoLayout是一个强大的自动布局库,它可以帮助开发者更轻松地实现Cell的高度自适应。下面我们将深入探讨如何利用SDAutoLayout来实现这一功能。 首先,了解SDAutoLayout的基本原理。SDAutoLayout是基于Masonry的...

    ios-Masonry对TableViewCell进行自动布局.zip

    总的来说,"ios-Masonry对TableViewCell进行自动布局.zip"这个项目展示了如何利用Masonry的便利性在TableViewCell中实现动态布局,以及如何通过监听用户点击来控制内容的展开和折叠。通过学习这个项目,开发者可以更...

    cell高度自适应

    这里我们将深入探讨如何基于`Masonry`框架实现`UITableView`中`cell`的高度自适应。 `Masonry`是一个强大的Auto Layout的宏定义库,它提供了简洁易用的API,使得布局工作变得更加高效。在`UITableView`中,我们通常...

    IOS7,Label自动换行,自适应高度

    然而,从iOS7开始,Apple引入了新的属性和方法,使得UILabel能够更方便地实现自动换行和自适应高度的功能。 首先,我们来看一下如何在iOS7及以后的版本中设置UILabel以实现自动换行。这个功能主要依赖于`...

    ios-Masonry Cell 动态高度 自动布局 Autolayout.zip

    `iOS-Masonry Cell 动态高度 自动布局 Autolayout.zip`这个压缩包中包含了一个名为`MASCellTest`的项目,它展示了如何利用Masonry库来实现UITableViewCell的动态高度计算。Masonry是一个基于AutoLayout的轻量级框架...

    TableView高度自适应

    本篇文章将深入探讨如何实现TableView的高度自适应,包括为tableView设置自适应高度、自定义cell以及利用Masonry进行约束布局。 首先,我们要理解TableView的高度自适应是通过计算每个cell的高度来实现的。在默认...

    UIScrollView自适应高度或宽度

    本教程将深入讲解如何实现UIScrollView的自适应高度或宽度,以达到内容自动填充的效果。我们将主要使用Objective-C进行演示,但Swift开发者也可以通过相似的思路来实现这一功能。 首先,我们要理解实现自适应高度或...

    Masonry Cell 自适应

    这个项目“Masonry Cell 自适应”显然关注的是如何利用Masonry库来实现UITableViewCell的高度自适应,以便在xcode6.4和iOS8环境下运行。在现代iOS开发中,动态高度的UITableViewCell能够更好地展示各种内容,提高...

    iOS使用masonry自适应布局

    本知识点将深入探讨如何使用Masonry来实现cell的高度自适应,以及如何构建一个简易的聊天界面布局。 首先,我们需要理解Masonry的基本用法。Masonry的核心在于它的`MASConstraintMaker`类,它提供了链式API来创建和...

    详解iOS tableViewCell自适应高度 第三发类库

    在github中有许多大牛封装好的第三发类库,其中有个自适应cell高度的类库 下载地址:https://github.com/gsdios/SDAutoLayout model类 commentsModel #import JSONModel.h #import getCommentData.h @interface ...

    iOS WKWebView和UIWebView自适应高度

    总的来说,无论是UIWebView还是WKWebView,实现自适应高度的关键在于正确地执行JavaScript代码以获取内容高度,并在适当的时候更新视图的布局。在iOS应用中,合理地利用这些技术,可以让用户获得更加流畅和自然的...

    TestAutoLayout002:利用Masonry实现屏幕尺寸适配和横竖转屏,cell根据文字内容实现高度宽度自适应

    MSAutoLayout 利用Masonry实现屏幕尺寸适配和横竖转屏,cell根据文字内容实现高度宽度自适应 #视频演示 第一次上传... :grinning_face_with_smiling_eyes:

    masonry.pkgd.min.js 自适应瀑布流插件

    "masonry.pkgd.min.js" 就是一个实现了这种布局效果的JavaScript插件,它具有自适应特性,能够根据浏览器窗口的大小变化动态调整布局。 **Masonry 插件详解** 1. **Masonry 概念** Masonry 插件是由 David ...

    Masonry_TableViewCellAutoHeight:使用masonry来动态获得tableviewCell高度

    通过这种方式,你可以利用Masonry的便利性,轻松地实现`UITableView`的自适应高度功能。在实际开发中,你可能还需要考虑更多细节,如性能优化、复用机制等,但以上步骤已经覆盖了使用Masonry动态计算`...

    UITableView自适应

    在这个话题中,我们将深入探讨两种主流方法来实现UITableView的高度自适应: Masonry布局和直接计算。 1. **Masonry布局** Masonry是一款强大的AutoLayout库,它简化了使用NSLayoutConstraint进行布局的过程。在...

    Masonry自动布局实践

    下面是如何使用Masonry和FDTemplateLayoutCell实现一个自适应高度的UITableViewCell的步骤: 1. **集成库**:首先,确保在项目中已经引入了Masonry和FDTemplateLayoutCell库。可以通过CocoaPods或者Carthage进行...

    ios-YYKit Masonry fd 高度自动计算, 纯代码高性能朋友圈!.zip

    利用Masonry 和YYlable ,FDTemplateLayout 布局的高性能朋友圈, 丝滑流畅 ! 代码有点大放不下, GitHub地址 : https://github.com/BB521/PenyouquanList.git 欢迎下载!!

    UITableViewCell自适应高度

    使用Masonry,我们可以更方便地创建和管理Cell内部视图的约束,从而实现自适应高度。 以下是实现步骤: 1. **初始化Cell**:在`tableView(_:numberOfRowsInSection:)`方法中,确保返回正确的行数。在`tableView(_:...

Global site tag (gtag.js) - Google Analytics