iOS UITableView5种行高自适应方案的选择

下面将给出了5种Cell自适应高度的方案,并对比每种实现方案的流畅度。从UI最不流畅的一种开始,我们慢慢优化。通过观察屏幕的FPS来判断屏幕在操作时是否卡顿。关于对FPS的实时监测,使用了YYKit-Demo中FPS控件来实现。点击列表中不同Cell都会跳转相同的内容列表页。只不过每个Cell所对应的内容页面的Cell自适应高度的实现方式不同。


5种Cell高度自适应方案

1.Autolayout + AutomaticDimension

点击第一个Cell进入的页面完全由AutoLayout进行布局,Cell自适应的高度也不用我们自己计算,而是使用系统提供的解决方案UITableViewAutomaticDimension来解决。当然,使用UITableViewAutomaticDimension要依赖于你添加的约束,稍后会介绍到。这种实现方案用起来简单,不过UI流畅度方面不太理想。当TableView快速滑动时,就会出现严重的掉帧。亲测FPS最低值38!


545446-20160922151158059-1130164887.gif
#//第1步:设置预估值
self.tableView.estimatedRowHeight = 100.0;  

#//第2步:返回UITableViewAutomaticDimension 自动调整约束,性能非常低
-  (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {

    return UITableViewAutomaticDimension;      
}
cell所有子控件和底部动态label的约束
动态label关键的约束,撑开cell

2.Autolayout + CountHeight

依然是采用AutoLayout的方式来对Cell的内容进行布局,不过Cell的高度我们是自己计算的,计算的过程是放在子线程中进行的,所以这种实现方式要优于第一种实现方式,亲测FPS最低值36!


手动计算行高,Autolayout布局
- (void)createDataSupport {

    self.dataSupport = [[DataSupport alloc] init];
    __weak typeof (self) weak_self = self;
    [self.dataSupport setUpdataDataSourceBlock:^(NSMutableArray *dataSource) {
        weak_self.dataSource = dataSource;
        [weak_self.tableView reloadData];
    }];

    [self addTestData];
}

- (void)addTestData {

    dispatch_queue_t concurrentQueue = dispatch_queue_create("zeluli.concurrent", DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_t group = dispatch_group_create();
    dispatch_semaphore_t lock = dispatch_semaphore_create(1);
    
    for (int i = 0; i < 50; i ++) {
        dispatch_group_async(group, concurrentQueue, ^{
            dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
            [self createTestModel];
            dispatch_semaphore_signal(lock);
        });
    }
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        [self updateDataSource];
    });
}

- (void)createTestModel {
    TestDataModel * model = [[TestDataModel alloc] init];
    model.title = @"行歌";
    
    NSDateFormatter *dataFormatter = [[NSDateFormatter alloc] init];
    [dataFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
    model.time = [dataFormatter stringFromDate:[NSDate date]];
    
    NSString *imageName = [NSString stringWithFormat:@"%d.jpg", arc4random() % 6];
    model.imageName =imageName;
    
    NSInteger endIndex = arc4random() % contentText.length;
    model.content = [contentText substringToIndex:endIndex];
    
    model.textHeight = [self countTextHeight:model.content];
    model.cellHeight = model.textHeight + 60;
    
    NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:model.content];
    text.font = [UIFont systemFontOfSize:14];
    text.lineSpacing = 3;
    model.attributeContent = text;
    
    model.attributeTitle = [[NSAttributedString alloc] initWithString:model.title];
    model.attributeTime = [[NSAttributedString alloc] initWithString:model.time];
    
    [self.dataSource addObject:model];
}

-(CGFloat)countTextHeight:(NSString *) text {

    NSMutableAttributedString *attributeString = [[NSMutableAttributedString alloc] initWithString:text];
    NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init];
    style.lineSpacing = 0;
    UIFont *font = [UIFont systemFontOfSize:14];
    [attributeString addAttribute:NSParagraphStyleAttributeName value:style range:NSMakeRange(0, text.length)];
    [attributeString addAttribute:NSFontAttributeName value:font range:NSMakeRange(0, text.length)];
    NSStringDrawingOptions options = NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading;
    CGRect rect = [attributeString boundingRectWithSize:CGSizeMake(SCREEN_WIDTH - 30, CGFLOAT_MAX) options:options context:nil];

    return rect.size.height + 40;
}

- (void)updateDataSource {

    if (self.updateDataBlock != nil) {
        self.updateDataBlock(self.dataSource);
    }
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    if (indexPath.row < self.dataSource.count) {
        TestDataModel *model = self.dataSource[indexPath.row];
        return model.cellHeight;
    }

    return 100;
}

@implementation AutolayoutTableViewCell

- (void)configCellData:(TestDataModel *)model {//配置cell中的数据
    
    [self.headerImageView setImage:[[ImageCache shareInstance] getCacheImage:model.imageName]];
    [self.titleLable setText:model.title];
    [self.timeLabel setText:model.time];
    [self.contentLabel setText:model.content];
}

3.FrameLayout + CountHeight

为了进一步提高流畅度,我们采用了纯Frame布局,因为Autolayout最终还是会被转换成Frame进行布局的,所以我们就用Frame对整个Cell中的所有子控件进行布局。当然Cell高度及可变内容的高度,跟第2种方法一样都是在子线程中进行计算的,这优化的重要一步。这种实现方式还是比较流畅的,可以作为折中的方案.亲测FPS最低值42!


手动计算行高,frame布局

4.YYKit + CountHeight

545446-20160922151158059-1130164887.gif

接下来我们继续进行优化,引入第三方UI组件YYKit。将Cell上的组件替换成YYKit所提供的组件。然后使用Frame进行布局,当然也是在子线程中对Cell的高度进行了计算。效果还是比较流畅的,但是还未达到完全不掉帧的效果。亲测FPS最低值53!

@property (strong, nonatomic) UIImageView *headerImageView;
@property (strong, nonatomic) YYLabel *titleLable;
@property (strong, nonatomic) YYLabel *timeLabel;
@property (strong, nonatomic) YYTextView *contentTextView;

5.AsyncDisplayKit + CountHeight

我们用Facebook提供的第三方库来进行基础组件的替换,将我们使用到的组件替换成AsyncDisplayKit相应的Note。这些Note是对系统组件的重组,对组件的显示进行了优化,让其渲染更为流畅,亲测FPS最低值59!
如果你对UI流畅度要求比较高的话,那么AsyncDisplayKit是一个比较好的选择。不过会严重依赖AsyncDisplayKit,如果AsyncDisplayKit停止维护了,后期对AsyncDisplayKit进行替换的话,工作量还是比较大的。因为这种布局框架不像网络框架,我们可以对网络框架的调用进行提取,网络层统一对外接口,很方便切换到其他网络请求库。但是像AsyncDisplayKit这种框架会散布于UI层的各个角落,封装提取不易,更不用说轻而易举的替换了。所以像这种页面的实现,个人还是偏向于Framelayout + CountHeight的方式来实现。

@property (strong, nonatomic) ASImageNode *headerImageNode;
@property (strong, nonatomic) ASTextNode *titleTextNode;
@property (strong, nonatomic) ASTextNode *timeTextNode;
@property (strong, nonatomic) ASTextNode *contentTextNode;

总结:

  • 1、2方案对比,手动计算行高优于自适应行高
  • 2、3方案对比,frame优于Autolayout
  • 3、4、5方案, 根据实际情况进行选择,方案5最优,缺点同样明显,侵入性强

参考资料:https://www.cnblogs.com/ludashi/p/5895725.html
参考资料:https://blog.ibireme.com/2015/11/12/smooth_user_interfaces_for_ios/

你可能感兴趣的:(iOS UITableView5种行高自适应方案的选择)