iOS中关于列表滚动流畅的一些探讨

移动开发 iOS
近些年,App 越来越推崇体验至上,随随便便乱写一通的话已经很难让用户买帐了,顺滑的列表便是其中很重要的一点。如果一个 App 的页面滚动起来总是卡顿卡顿的,轻则被当作反面教材来吐槽或者衬托“我们的 App balabala…”,重则直接卸载。正好最近在优化这一块儿,总结记录下。

近些年,App 越来越推崇体验至上,随随便便乱写一通的话已经很难让用户买帐了,顺滑的列表便是其中很重要的一点。如果一个 App 的页面滚动起来总是卡顿卡顿的,轻则被当作反面教材来吐槽或者衬托“我们的 App balabala…”,重则直接卸载。正好最近在优化这一块儿,总结记录下。

如果说有什么好的博客文章推荐,ibireme 的 iOS 保持界面流畅的技巧 这篇堪称业界毒瘤,墙裂推荐反复阅读。这篇文章中讲解了很多的优化点,我自己总结了下收益***的两个优化点:

  • 避免重复多次计算 cell 行高
  • 文本异步渲染

iOS 中关于列表滚动流畅的一些探讨

大家可以看看上面这张图的对比分析,数据是 iPhone6 的机子用 instruments 抓的,左边的是用 Auto Layout 绘制界面的数据分析,正常如果想平滑滚动的话,fps 至少需要稳定在 55 左右,我们可以发现,在没有缓存行高和异步渲染的情况下 fps 是***的,可以说是比较卡顿了,至少是能肉眼感觉出来,能满足平滑滚动要求的也只有在缓存行高且异步渲染的情况下;右边的是没用 Auto Layout 直接用 frame 来绘制界面的数据分析,可以发现即使没有异步渲染,也能勉强满足平滑滚动的要求,如果开启异步渲染的话,可以说是相当的丝滑了。

避免重复多次计算 cell 行高

TableView 行高计算可以说是个老生常谈的问题了, heightForRowAtIndexPath: 是个调用相当频繁的方法,在里面做过多的事情难免会造成卡顿。 在 iOS 8 中,我们可以通过设置下面两个属性来很轻松的实现高度自适应: 

  1. self.tableView.estimatedRowHeight = 88; 
  2. self.tableView.rowHeight = UITableViewAutomaticDimension; 

 虽然很方便,不过如果你的页面对性能有一定要求,建议不要这么做,具体可以看看 sunnyxx 的 优化UITableViewCell高度计算的那些事 。文中针对 Auto Layout,提供了个 cell 行高的缓存库 UITableView-FDTemplateLayoutCell ,可以很好的帮助我们避免 cell 行高多次计算的问题。

如果不使用 Auto Layout,我们可以在请求完拿到数据后提前计算好页面个个控件的 frame 和 cell 高度,并且缓存在内存中,用的时候直接在 heightForRowAtIndexPath: 取出计算好的值就行,大概流程如下:

模拟请求数据回调: 

  1. - (void)viewDidLoad { 
  2.     [super viewDidLoad]; 
  3.      
  4.     [self buildTestDataThen:^(NSMutableArray <FDFeedEntity *> *entities) { 
  5.         self.data = @[].mutableCopy; 
  6.         @autoreleasepool { 
  7.             for (FDFeedEntity *entity in entities) { 
  8.                 FrameModel *frameModel = [FrameModel new]; 
  9.                 frameModel.entity = entity; 
  10.                 [self.data addObject:frameModel]; 
  11.             } 
  12.         } 
  13.         [self.tvFeed reloadData]; 
  14.     }]; 

一个简单计算 frame 、cell 行高方式: 

  1. //FrameModel.h 
  2.  
  3. @interface FrameModel : NSObject 
  4.  
  5. @property (assign, nonatomic, readonly) CGRect titleFrame; 
  6. @property (assign, nonatomic, readonly) CGFloat cellHeight; 
  7. @property (strong, nonatomic) FDFeedEntity *entity; 
  8.  
  9. @end 
  10.  
  11. //FrameModel.m 
  12.  
  13. @implementation FrameModel 
  14.  
  15. - (void)setEntity:(FDFeedEntity *)entity { 
  16.     if (!entity) return
  17.      
  18.     _entity = entity; 
  19.      
  20.     CGFloat maxLayout = ([UIScreen mainScreen].bounds.size.width - 20.f); 
  21.     CGFloat bottom = 4.f; 
  22.      
  23.     //title 
  24.     CGFloat titleX = 10.f; 
  25.     CGFloat titleY = 10.f; 
  26.     CGSize titleSize = [entity.title boundingRectWithSize:CGSizeMake(maxLayout, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading attributes:@{NSFontAttributeName : Font(16.f)} context:nil].size
  27.     _titleFrame = CGRectMake(titleX, titleY, titleSize.width, titleSize.height); 
  28.      
  29.     //cell Height 
  30.     _cellHeight = (CGRectGetMaxY(_titleFrame) + bottom); 
  31.  
  32. @end 

行高取值: 

  1. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 
  2.     FrameFeedCell *cell = [tableView dequeueReusableCellWithIdentifier:FrameFeedCellIdentifier forIndexPath:indexPath]; 
  3.     FrameModel *frameModel = self.data[indexPath.row]; 
  4.     cell.model = frameModel; 
  5.     return cell; 
  6.  
  7. - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { 
  8.     FrameModel *frameModel = self.data[indexPath.row]; 
  9.     return frameModel.cellHeight; 

控件赋值: 

  1. - (void)setModel:(FrameModel *)model { 
  2.     if (!model) return
  3.      
  4.     _model = model; 
  5.      
  6.     FDFeedEntity *entity = model.entity; 
  7.      
  8.     self.titleLabel.frame = model.titleFrame; 
  9.     self.titleLabel.text = entity.title; 

优缺点

缓存行高方式有现成的库简单方便,虽然 UITableView-FDTemplateLayoutCell 已经处理的很好了,但是 Auto Layout 对性能还是会有部分消耗;手动计算 frame 方式所有的位置都需要计算,比较麻烦,而且在数据量很大的情况下,大量的计算对数据展示时间会有部分影响,相应的回报就是性能会更好一些。

文本异步渲染

当显示大量文本时,CPU 的压力会非常大。对此解决方案只有一个,那就是自定义文本控件,用 TextKit 或***层的 CoreText 对文本异步绘制。尽管这实现起来非常麻烦,但其带来的优势也非常大,CoreText 对象创建好后,能直接获取文本的宽高等信息,避免了多次计算(调整 UILabel 大小时算一遍、UILabel 绘制时内部再算一遍);CoreText 对象占用内存较少,可以缓存下来以备稍后多次渲染。

幸运的是,想支持文本异步渲染也有现成的库 YYText ,下面来讲讲如何搭配它***程度满足我们如丝般顺滑的需求:

Frame 搭配异步渲染

基本思路和计算 frame 类似,只不过把系统的 boundingRectWithSize: 、 sizeWithAttributes: 换成 YYText 中的方法:

配置 frame model: 

  1. //FrameYYModel.h 
  2.  
  3. @interface FrameYYModel : NSObject 
  4.  
  5. @property (assign, nonatomic, readonly) CGRect titleFrame; 
  6. @property (strong, nonatomic, readonly) YYTextLayout *titleLayout; 
  7.  
  8. @property (assign, nonatomic, readonly) CGFloat cellHeight; 
  9.  
  10. @property (strong, nonatomic) FDFeedEntity *entity; 
  11.  
  12. @end 
  13.  
  14. //FrameYYModel.m 
  15.  
  16. @implementation FrameYYModel 
  17.  
  18. - (void)setEntity:(FDFeedEntity *)entity { 
  19.     if (!entity) return
  20.      
  21.     _entity = entity; 
  22.      
  23.     CGFloat maxLayout = ([UIScreen mainScreen].bounds.size.width - 20.f); 
  24.     CGFloat space = 10.f; 
  25.     CGFloat bottom = 4.f; 
  26.      
  27.     //title 
  28.     NSMutableAttributedString *title = [[NSMutableAttributedString alloc] initWithString:entity.title]; 
  29.     title.yy_font = Font(16.f); 
  30.     title.yy_color = [UIColor blackColor]; 
  31.      
  32.     YYTextContainer *titleContainer = [YYTextContainer containerWithSize:CGSizeMake(maxLayout, CGFLOAT_MAX)]; 
  33.     _titleLayout = [YYTextLayout layoutWithContainer:titleContainer text:title]; 
  34.      
  35.     CGFloat titleX = 10.f; 
  36.     CGFloat titleY = 10.f; 
  37.     CGSize titleSize = _titleLayout.textBoundingSize; 
  38.     _titleFrame = (CGRect){titleX,titleY,CGSizeMake(titleSize.width, titleSize.height)}; 
  39.      
  40.     //cell Height 
  41.     _cellHeight = (CGRectGetMaxY(_titleFrame) + bottom); 
  42.  
  43. @end 

对比上面 frame,可以发现多了个 YYTextLayout 属性,这个属性可以提前配置文本的特性,包括 font 、 textColor 以及行数、行间距、内间距等等,好处就是可以把一些逻辑提前处理好,比如根据接口字段,动态配置字体颜色,字号等,如果用 Auto Layout,这部分逻辑则不可避免的需要写在 cellForRowAtIndexPath: 方法中。

UITableViewCell 处理 : 

  1. - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { 
  2.     self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; 
  3.     if (!self) return nil; 
  4.    
  5.     YYLabel *title = [YYLabel new]; 
  6.     title.displaysAsynchronously = YES; //开启异步渲染 
  7.     title.ignoreCommonProperties = YES; //忽略属性 
  8.     title.layer.borderColor = [UIColor brownColor].CGColor; 
  9.     title.layer.cornerRadius = 1.f; 
  10.     title.layer.borderWidth = 1.f; 
  11.     [self.contentView addSubview:_titleLabel = title]; 
  12.    
  13.     return self; 

赋值: 

  1. - (void)setModel:(FrameYYModel *)model { 
  2.     if (!model) return
  3.     _model = model; 
  4.      
  5.     self.titleLabel.frame = model.titleFrame; 
  6.     self.titleLabel.textLayout = model.titleLayout; //直接取 YYTextLayout 

 Auto Layout 搭配异步渲染

YYText 非常友好,同样支持 xib,YYText 继承自 UIView ,正常的在 xib 中配置约束就行了,需要注意的一点是,多行文本的情况下需要设置***换行宽: 

  1. CGFloat maxLayout = [UIScreen mainScreen].bounds.size.width - 20.f; 
  2. self.titleLabel.preferredMaxLayoutWidth = maxLayout; 
  3. self.subTitleLabel.preferredMaxLayoutWidth = maxLayout; 
  4. self.contentLabel.preferredMaxLayoutWidth = maxLayout; 

 优缺点

YYText 的异步渲染能极大程度的提高列表流畅度,真正达到如丝般顺滑,但是在开启异步时,刷新列表会有闪烁情况,不知道算不算 bug,最近也看到作者回归了,相信这个库会越来越好,毕竟 真●大神!

其它

列表中如果存在很多系统设置的圆角页面导致卡顿: 

  1. label.layer.cornerRadius = 5.f; 
  2. label.clipsToBounds = YES; 

 其实据我观察,只要当前屏幕内只要设置圆角的控件个数不要太多(大概十几个算个零界点),就不会引起卡顿。

还有就是只要不设置 clipsToBounds 不管多少个,都不会卡顿,比如你需要圆角的控件是白色背景色的,然后它的父控件也是白色背景色的,而且没有点击后高亮的,就没必要 clipsToBounds 了。

总结

YYText 和 UITableView-FDTemplateLayoutCell 搭配可以很大程度的提高列表流畅度,如果时间比较紧迫,可以直接采取 Auto Layout + UITableView-FDTemplateLayoutCell + YYText 方式;如果列表中文本不包含富文本,仅仅显示文字,又不想引入这两个库,可以使用系统方式提前计算 Frame;如果想***程度的流畅度,就需要使用 提前计算 Frame + YYText,具体大家根据自己情况选择合适的方案就行。

责任编辑:未丽燕 来源: ifelseboyxx's Blog
相关推荐

2017-02-20 16:28:30

DCISDN-WAN传输网络

2022-01-12 08:30:55

结构体指针STM32

2009-03-13 09:31:03

.NET整合分布式应用

2011-07-13 09:13:56

Android设计

2009-07-02 10:52:30

JavaBean规范

2009-11-25 09:23:47

PHP引用&符号

2023-02-10 09:46:04

bash脚本变量

2022-11-09 19:02:10

Linux

2013-04-07 10:40:55

前端框架前端

2009-06-18 09:51:25

Java继承

2012-09-25 10:03:56

JavaJava封面Java开发

2011-03-11 09:27:11

Java性能监控

2012-04-19 10:06:55

微软Windows 8 E

2009-06-04 16:28:43

EJB常见问题

2015-12-04 10:04:53

2017-12-21 07:54:07

2022-04-14 10:22:44

故事卡业务

2020-09-28 06:45:42

故障复盘修复

2021-06-10 10:02:19

优化缓存性能

2016-10-18 22:10:02

HTTP推送HTML
点赞
收藏

51CTO技术栈公众号