UITableView 优化
前言
UITableView 是我们开发中常用到的控件。其优化也是老生常谈的话题。笔者在这里抛砖引玉。
image圆角问题
IM模块的头像, 笔者的项目用UIButton。
早就听说iOS 设置圆角会造成性能上的开销。设置cornerRadius和masksToBounds 会发生离屏渲染。
但在iOS 9后,苹果对圆角问题进行了优化。UIImageView里png图片通过以上属性设置圆角不会触发离屏渲染(在iOS 12.1下亲测)。但UIButton设置图片和圆角,UILabel设置layer.backgroundColor 均会造成离屏渲染。
image- 一种方法是在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代理执行顺序说heightForRowAtIndexPath 在 cellForRowAtIndexPath前。笔者下载Demo来测试确实如此。
但笔者自己写了一份Demo,亲测并不是。
image于是在一篇文章tableView代理方法执行顺序中发现真相。
image其实文档中也说清了,实现了预期高度,实际高度方法会被延迟到 cell将要显示时 调用。
image
对于固定高度的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
简单的说,设置预算高度和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。