近些年,App 越来越推崇体验至上,随随便便乱写一通的话已经很难让用户买帐了,顺滑的列表便是其中很重要的一点。如果一个 App 的页面滚动起来总是卡顿卡顿的,轻则被当作反面教材来吐槽或者衬托“我们的 App balabala…”,重则直接卸载。正好最近在优化这一块儿,总结记录下。
如果说有什么好的博客文章推荐,ibireme 的 iOS 保持界面流畅的技巧 这篇堪称业界毒瘤,墙裂推荐反复阅读。这篇文章中讲解了很多的优化点,我自己总结了下收益***的两个优化点:
避免重复多次计算 cell 行高
文本异步渲染
大家可以看看上面这张图的对比分析,数据是 iPhone6 的机子用 instruments 抓的,左边的是用 Auto Layout 绘制界面的数据分析,正常如果想平滑滚动的话,fps 至少需要稳定在 55 左右,我们可以发现,在没有缓存行高和异步渲染的情况下 fps 是***的,可以说是比较卡顿了,至少是能肉眼感觉出来,能满足平滑滚动要求的也只有在缓存行高且异步渲染的情况下;右边的是没用 Auto Layout 直接用 frame 来绘制界面的数据分析,可以发现即使没有异步渲染,也能勉强满足平滑滚动的要求,如果开启异步渲染的话,可以说是相当的丝滑了。
避免重复多次计算 cell 行高
TableView 行高计算可以说是个老生常谈的问题了, heightForRowAtIndexPath: 是个调用相当频繁的方法,在里面做过多的事情难免会造成卡顿。 在 iOS 8 中,我们可以通过设置下面两个属性来很轻松的实现高度自适应:
复制
self.tableView.estimatedRowHeight = 88; self.tableView.rowHeight = UITableViewAutomaticDimension;
1.
2.
虽然很方便,不过如果你的页面对性能有一定要求,建议不要这么做,具体可以看看 sunnyxx 的 优化UITableViewCell高度计算的那些事 。文中针对 Auto Layout,提供了个 cell 行高的缓存库 UITableView-FDTemplateLayoutCell ,可以很好的帮助我们避免 cell 行高多次计算的问题。
如果不使用 Auto Layout,我们可以在请求完拿到数据后提前计算好页面个个控件的 frame 和 cell 高度,并且缓存在内存中,用的时候直接在 heightForRowAtIndexPath: 取出计算好的值就行,大概流程如下:
模拟请求数据回调:
复制
- (void)viewDidLoad { [super viewDidLoad]; [self buildTestDataThen:^(NSMutableArray <FDFeedEntity *> *entities) { self.data = @[].mutableCopy; @autoreleasepool { for (FDFeedEntity *entity in entities) { FrameModel *frameModel = [FrameModel new]; frameModel.entity = entity; [self.data addObject:frameModel]; } } [self.tvFeed reloadData]; }]; }
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
一个简单计算 frame 、cell 行高方式:
复制
//FrameModel.h @interface FrameModel : NSObject @property (assign, nonatomic, readonly) CGRect titleFrame; @property (assign, nonatomic, readonly) CGFloat cellHeight; @property (strong, nonatomic) FDFeedEntity *entity; @end //FrameModel.m @implementation FrameModel - (void)setEntity:(FDFeedEntity *)entity { if (!entity) return; _entity = entity; CGFloat maxLayout = ([UIScreen mainScreen].bounds.size.width - 20.f); CGFloat bottom = 4.f; //title CGFloat titleX = 10.f; CGFloat titleY = 10.f; CGSize titleSize = [entity.title boundingRectWithSize:CGSizeMake(maxLayout, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading attributes:@{NSFontAttributeName : Font(16.f)} context:nil].size; _titleFrame = CGRectMake(titleX, titleY, titleSize.width, titleSize.height); //cell Height _cellHeight = (CGRectGetMaxY(_titleFrame) + bottom); } @end
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
行高取值:
复制
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { FrameFeedCell *cell = [tableView dequeueReusableCellWithIdentifier:FrameFeedCellIdentifier forIndexPath:indexPath]; FrameModel *frameModel = self.data[indexPath.row]; cell.model = frameModel; return cell; } - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { FrameModel *frameModel = self.data[indexPath.row]; return frameModel.cellHeight; }
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
控件赋值:
复制
- (void)setModel:(FrameModel *)model { if (!model) return; _model = model; FDFeedEntity *entity = model.entity; self.titleLabel.frame = model.titleFrame; self.titleLabel.text = entity.title; }
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
优缺点
缓存行高方式有现成的库简单方便,虽然 UITableView-FDTemplateLayoutCell 已经处理的很好了,但是 Auto Layout 对性能还是会有部分消耗;手动计算 frame 方式所有的位置都需要计算,比较麻烦,而且在数据量很大的情况下,大量的计算对数据展示时间会有部分影响,相应的回报就是性能会更好一些。
文本异步渲染
当显示大量文本时,CPU 的压力会非常大。对此解决方案只有一个,那就是自定义文本控件,用 TextKit 或***层的 CoreText 对文本异步绘制。尽管这实现起来非常麻烦,但其带来的优势也非常大,CoreText 对象创建好后,能直接获取文本的宽高等信息,避免了多次计算(调整 UILabel 大小时算一遍、UILabel 绘制时内部再算一遍);CoreText 对象占用内存较少,可以缓存下来以备稍后多次渲染。
幸运的是,想支持文本异步渲染也有现成的库 YYText ,下面来讲讲如何搭配它***程度满足我们如丝般顺滑的需求:
Frame 搭配异步渲染
基本思路和计算 frame 类似,只不过把系统的 boundingRectWithSize: 、 sizeWithAttributes: 换成 YYText 中的方法:
配置 frame model:
复制
//FrameYYModel.h @interface FrameYYModel : NSObject @property (assign, nonatomic, readonly) CGRect titleFrame; @property (strong, nonatomic, readonly) YYTextLayout *titleLayout; @property (assign, nonatomic, readonly) CGFloat cellHeight; @property (strong, nonatomic) FDFeedEntity *entity; @end //FrameYYModel.m @implementation FrameYYModel - (void)setEntity:(FDFeedEntity *)entity { if (!entity) return; _entity = entity; CGFloat maxLayout = ([UIScreen mainScreen].bounds.size.width - 20.f); CGFloat space = 10.f; CGFloat bottom = 4.f; //title NSMutableAttributedString *title = [[NSMutableAttributedString alloc] initWithString:entity.title]; title.yy_font = Font(16.f); title.yy_color = [UIColor blackColor]; YYTextContainer *titleContainer = [YYTextContainer containerWithSize:CGSizeMake(maxLayout, CGFLOAT_MAX)]; _titleLayout = [YYTextLayout layoutWithContainer:titleContainer text:title]; CGFloat titleX = 10.f; CGFloat titleY = 10.f; CGSize titleSize = _titleLayout.textBoundingSize; _titleFrame = (CGRect){titleX,titleY,CGSizeMake(titleSize.width, titleSize.height)}; //cell Height _cellHeight = (CGRectGetMaxY(_titleFrame) + bottom); } @end
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
对比上面 frame,可以发现多了个 YYTextLayout 属性,这个属性可以提前配置文本的特性,包括 font 、 textColor 以及行数、行间距、内间距等等,好处就是可以把一些逻辑提前处理好,比如根据接口字段,动态配置字体颜色,字号等,如果用 Auto Layout,这部分逻辑则不可避免的需要写在 cellForRowAtIndexPath: 方法中。
UITableViewCell 处理 :
复制
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; if (!self) return nil; YYLabel *title = [YYLabel new]; title.displaysAsynchronously = YES; //开启异步渲染 title.ignoreCommonProperties = YES; //忽略属性 title.layer.borderColor = [UIColor brownColor].CGColor; title.layer.cornerRadius = 1.f; title.layer.borderWidth = 1.f; [self.contentView addSubview:_titleLabel = title]; return self; }
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
赋值:
复制
- (void)setModel:(FrameYYModel *)model { if (!model) return; _model = model; self.titleLabel.frame = model.titleFrame; self.titleLabel.textLayout = model.titleLayout; //直接取 YYTextLayout }
1.
2.
3.
4.
5.
6.
7.
Auto Layout 搭配异步渲染
YYText 非常友好,同样支持 xib,YYText 继承自 UIView ,正常的在 xib 中配置约束就行了,需要注意的一点是,多行文本的情况下需要设置***换行宽:
复制
CGFloat maxLayout = [UIScreen mainScreen].bounds.size.width - 20.f; self.titleLabel.preferredMaxLayoutWidth = maxLayout; self.subTitleLabel.preferredMaxLayoutWidth = maxLayout; self.contentLabel.preferredMaxLayoutWidth = maxLayout;
1.
2.
3.
4.
优缺点
YYText 的异步渲染能极大程度的提高列表流畅度,真正达到如丝般顺滑,但是在开启异步时,刷新列表会有闪烁情况,不知道算不算 bug,最近也看到作者回归了,相信这个库会越来越好,毕竟 真●大神!
其它
列表中如果存在很多系统设置的圆角页面导致卡顿:
复制
label.layer.cornerRadius = 5.f; label.clipsToBounds = YES;
1.
2.
其实据我观察,只要当前屏幕内只要设置圆角的控件个数不要太多(大概十几个算个零界点),就不会引起卡顿。
还有就是只要不设置 clipsToBounds 不管多少个,都不会卡顿,比如你需要圆角的控件是白色背景色的,然后它的父控件也是白色背景色的,而且没有点击后高亮的,就没必要 clipsToBounds 了。
总结
YYText 和 UITableView-FDTemplateLayoutCell 搭配可以很大程度的提高列表流畅度,如果时间比较紧迫,可以直接采取 Auto Layout + UITableView-FDTemplateLayoutCell + YYText 方式;如果列表中文本不包含富文本,仅仅显示文字,又不想引入这两个库,可以使用系统方式提前计算 Frame;如果想***程度的流畅度,就需要使用 提前计算 Frame + YYText,具体大家根据自己情况选择合适的方案就行。