UITableView 优化

1,226 阅读4分钟

前言

UITableView 是我们开发中常用到的控件。其优化也是老生常谈的话题。笔者在这里抛砖引玉。


圆角问题

IM模块的头像, 笔者的项目用UIButton。

早就听说iOS 设置圆角会造成性能上的开销。设置cornerRadius和masksToBounds 会发生离屏渲染。

但在iOS 9后,苹果对圆角问题进行了优化。UIImageView里png图片通过以上属性设置圆角不会触发离屏渲染(在iOS 12.1下亲测)。但UIButton设置图片和圆角,UILabel设置layer.backgroundColor 均会造成离屏渲染。

iOS 高效添加圆角效果实战讲解

  • 一种方法是在drawRect中用CAShapeLayer 和 UIBezierPath。

这方法会导致内存暴增,还会离屏渲染。并没有优化,反而恶化了。

  • 还有一种方法,Core Graphics画出圆角矩形,UIImageView直接截取圆角图片。

此方法用CPU渲染。CPU渲染能力不如GPU,但圆角这种轻量级渲染,CPU还是能胜任的。重点是GPU离屏渲染需要上下文切换,严重时会造成卡顿。

此方法缺点,CPU以及内存 额外开销。


cell中部分view的复用

这里说的并不是cell的复用,而是cell中部分view 的复用。

IM模块中,消息发送状态view,有三种情况,发送中圈圈,发送失败感叹号,发送成功没有发送状态view。

由于大多数消息都是发送成功的,所以有 发送状态view 的cell比较少,一个界面可能最多就一两个状态view。每个cell都创建会浪费内存。

(当然日常项目,UIScrollView一样的view也可以类似思路优化)

思路:新建一个类ViewCache。两个数组,一个装着正在用的view,另一个装着缓存中的view。

当cell设置model时,如果发送失败状态,cell没有statusView,就取缓存数组取,缓存数组空就新建一个,并且放到正在用的view数组中。当不用时,就放回缓存数组中。

- (void)setModel:(CellModel *)model {
    switch (model.status) {
        case 失败:
            if (!self.statusView) {
                self.statusView = [self.viewCache dequeueStatusView]
                [self.contentView addSubView:self.statusView];
            }
            self.statusView.frame = model.layout.statusViewFrame;
            break;
        case 发送中:
            // 差不多
            break;
        default:
            if (self.statusView) {
                [self.viewCache removeStatusView:self.statusView];
                [self.statusView removeFromSuperview];
            }
            break;
    }
}

高度计算

先来了解数据源、代理方法的调用时机。

网上有文章iOS开发-简单科普下UITableView和UICollectionView代理执行顺序heightForRowAtIndexPathcellForRowAtIndexPath前。笔者下载Demo来测试确实如此。

但笔者自己写了一份Demo,亲测并不是。

于是在一篇文章tableView代理方法执行顺序中发现真相。

其实文档中也说清了,实现了预期高度,实际高度方法会被延迟到 cell将要显示时 调用。

对于固定高度的cell,直接设置rowHeight,不要实现代理cell高度方法。

我们知道 实现代理方法后, rowHeight 会失效。所以笔者伪一下代码(当然无凭无证乱猜)

    if(self.delegate && [self.delegate respondsToSelector:@selector(tableView:cellForRowAtIndexPath:)]) {
        return [self.delegate tableView:self cellForRowAtIndexPath:indexPath];
    } else {
        return self.rowHeight;// 默认高度44
    }

所以我们也就能节省两个方法(respondsToSelector和高度方法)的开销。(苹果有没有针对这部分做优化不得而知)

对于动态高度的cell

动态高度有两种方法,一种是利用AutoLayout,另一种是直接算frame。

  • AutoLayout

iOS-谈一谈自适应Cell的高度缓存

简单的说,设置预算高度和estimatedRowHeight = UITableViewAutomaticDimension,然后cell中最下面的控件设置底部约束,撑开cell。

这方法不用实现高度代理方法,滑动条会在滚动过程中重新调整。

但是AutoLayout最终需要转成frame。这里就无可避免开销比直接算frame大。所以如果cell很复杂,不建议用AutoLayout。

缓存高度需要用以下两个方法,返回Auto Layout后内容高度。

- (CGSize)systemLayoutSizeFittingSize:(CGSize)targetSize NS_AVAILABLE_IOS(6_0); 
- (CGSize)systemLayoutSizeFittingSize:(CGSize)targetSize withHorizontalFittingPriority:(UILayoutPriority)horizontalFittingPriority verticalFittingPriority:(UILayoutPriority)verticalFittingPriority NS_AVAILABLE_IOS(8_0);

具体实现

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    CellModel * model = self.models[indexPath.row];
    return model.cellHeight ?: UITableViewAutomaticDimension;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    CellModel * model = self.models[indexPath.row];
    TestCell * cell = [TestCell cellForTableView:tableView model:model];
    
    //高度缓存
    if (!model.cellHeight) {
        CGFloat height = [cell systemLayoutSizeFittingSize:CGSizeMake(tableView.frame.size.width, 0) withHorizontalFittingPriority:UILayoutPriorityRequired verticalFittingPriority:UILayoutPriorityFittingSizeLevel].height;
        model.cellHeight = height;
    }
    
    return cell;
}
  • 另一种直接算frame。

在model中,设置和布局相关的属性,cellHeight懒加载。(笔者项目封装了一个CellLayout对象,包含每个控件的frame以及cell高度)

- (CGFloat)cellHeight {
    if (!_cellHeight) {
        CGFloat iconH = 20;
        CGFloat contentH = [self contentH];//算出来
        _cellHeight = iconH + contentH + 10;
    }
    return _cellHeight;
}

然后在代理方法中

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    CellModel *model = self.models[indexPath.row];
    return model.cellHeight;
}

iOS开发之UITableview之多种Cell高度自适应实现方案的UI流畅度分析


笔者能力有限,除了以上的优化策略,其实UITableView还有很多能优化的地方。以后笔者如果有机会,会尝试往以下方向优化。

  • 利用RunLoop空闲时间,预计算未显示的Cell高度。(可以参考SDWebImage)
  • 异步绘制Cell。
  • 滑动手指松开时,描绘计算要显示的Cell。

参考