近些年,App 愈来愈推崇体验至上,随随便便乱写一通的话已经很难让用户买账了,顺滑的列表即是其中很重要的一点。若是一个 App 的页面滚动起来老是卡顿卡顿的,轻则被看成反面教材来吐槽或者陪衬“咱们的 App balabala...”,重则直接卸载。正好最近在优化这一起,总结记录下。ios
若是说有什么好的博客文章推荐,ibireme 的 iOS 保持界面流畅的技巧 这篇堪称业界经典,墙裂推荐反复阅读。这篇文章中讲解了不少的优化点,我本身总结了下收益最大的两个优化点:git
你们能够看看上面这张图的对比分析,数据是 iPhone6 的机子用 instruments 抓的,左边的是用 Auto Layout 绘制界面的数据分析,正常若是想平滑滚动的话,fps 至少须要稳定在 55 左右,咱们能够发现,在没有缓存行高和异步渲染的状况下 fps 是最低的,能够说是比较卡顿了,至少是能肉眼感受出来,能知足平滑滚动要求的也只有在缓存行高且异步渲染的状况下;右边的是没用 Auto Layout 直接用 frame 来绘制界面的数据分析,能够发现即便没有异步渲染,也能勉强知足平滑滚动的要求,若是开启异步渲染的话,能够说是至关的丝滑了。github
TableView 行高计算能够说是个老生常谈的问题了,heightForRowAtIndexPath:
是个调用至关频繁的方法,在里面作过多的事情不免会形成卡顿。 在 iOS 8 中,咱们能够经过设置下面两个属性来很轻松的实现高度自适应:objective-c
self.tableView.estimatedRowHeight = 88; self.tableView.rowHeight = UITableViewAutomaticDimension;
虽然很方便,不过若是你的页面对性能有必定要求,建议不要这么作,具体能够看看 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]; }]; }
//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
- (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; }
- (void)setModel:(FrameModel *)model { if (!model) return; _model = model; FDFeedEntity *entity = model.entity; self.titleLabel.frame = model.titleFrame; self.titleLabel.text = entity.title; }
缓存行高方式有现成的库简单方便,虽然 UITableView-FDTemplateLayoutCell 已经处理的很好了,可是 Auto Layout 对性能仍是会有部分消耗;手动计算 frame 方式全部的位置都须要计算,比较麻烦,并且在数据量很大的状况下,大量的计算对数据展现时间会有部分影响,相应的回报就是性能会更好一些。性能
当显示大量文本时,CPU 的压力会很是大。对此解决方案只有一个,那就是自定义文本控件,用 TextKit 或最底层的 CoreText 对文本异步绘制。尽管这实现起来很是麻烦,但其带来的优点也很是大,CoreText 对象建立好后,能直接获取文本的宽高等信息,避免了屡次计算(调整 UILabel 大小时算一遍、UILabel 绘制时内部再算一遍);CoreText 对象占用内存较少,能够缓存下来以备稍后屡次渲染。字体
幸运的是,想支持文本异步渲染也有现成的库 YYText ,下面来说讲如何搭配它最大程度知足咱们如丝般顺滑的需求:优化
基本思路和计算 frame 相似,只不过把系统的 boundingRectWithSize:
、 sizeWithAttributes:
换成 YYText 中的方法:ui
//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
对比上面 frame,能够发现多了个 YYTextLayout
属性,这个属性能够提早配置文本的特性,包括 font
、textColor
以及行数、行间距、内间距等等,好处就是能够把一些逻辑提早处理好,好比根据接口字段,动态配置字体颜色,字号等,若是用 Auto Layout,这部分逻辑则不可避免的须要写在 cellForRowAtIndexPath:
方法中。
- (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; }
- (void)setModel:(FrameYYModel *)model { if (!model) return; _model = model; self.titleLabel.frame = model.titleFrame; self.titleLabel.textLayout = model.titleLayout; //直接取 YYTextLayout }
YYText 很是友好,一样支持 xib,YYText 继承自 UIView
,所要作的事情也很简单:
开启异步属性能够代码里设置,也能够直接在 xib 里设置,分别以下:
self.titleLabel.displaysAsynchronously = YES; self.subTitleLabel.displaysAsynchronously = YES; self.contentLabel.displaysAsynchronously = YES; self.usernameLabel.displaysAsynchronously = YES; self.timeLabel.displaysAsynchronously = YES;
另外须要注意的一点是,多行文本的状况下须要设置最大换行宽:
CGFloat maxLayout = [UIScreen mainScreen].bounds.size.width - 20.f; self.titleLabel.preferredMaxLayoutWidth = maxLayout; self.subTitleLabel.preferredMaxLayoutWidth = maxLayout; self.contentLabel.preferredMaxLayoutWidth = maxLayout;
YYText 的异步渲染能极大程度的提升列表流畅度,真正达到如丝般顺滑,可是在开启异步时,刷新列表会有闪烁状况,仔细想一想以为也正常,毕竟是异步的,渲染也须要时间,这里做者给出了一些 方案,你们能够看看。
列表中若是存在不少系统设置的圆角页面致使卡顿:
label.layer.cornerRadius = 5.f; label.clipsToBounds = YES;
其实据我观察,只要当前屏幕内只要设置圆角的控件个数不要太多(大概十几个算个临界点),就不会引发卡顿。
还有就是只要不设置 clipsToBounds
无论多少个,都不会卡顿,好比你须要圆角的控件是白色背景色的,而后它的父控件也是白色背景色的,并且没有点击后高亮的,就不必 clipsToBounds 了。
咱们能够利用 instruments 中的 Time Profiler 来帮助咱们定位问题位置,选中 Xcode,command + control + i 打开:
咱们选中主线程,去掉系统的方法,而后操做一下列表,再截取一段调用信息,能够发现咱们本身实现的方法并无消耗多少时间,反而是系统的方法很费时,这也是卡顿的缘由:
另外有的人 instruments 看不到方法调用栈(右边一堆黑色的方法信息),去 Xcode 设置下就好了:
YYText 和 UITableView-FDTemplateLayoutCell 搭配能够很大程度的提升列表流畅度:
最后附上 Demo