iOS 中關於列表滾動流暢方案的一些探討

近些年,App 愈來愈推崇體驗至上,隨隨便便亂寫一通的話已經很難讓用戶買賬了,順滑的列表即是其中很重要的一點。若是一個 App 的頁面滾動起來老是卡頓卡頓的,輕則被看成反面教材來吐槽或者陪襯「咱們的 App balabala...」,重則直接卸載。正好最近在優化這一起,總結記錄下。ios

若是說有什麼好的博客文章推薦,ibireme 的 iOS 保持界面流暢的技巧 這篇堪稱經典,牆裂推薦反覆閱讀。這篇文章中講解了不少的優化點,我本身總結了下收益最大的兩個優化點:git

  • 避免重複屢次計算 cell 行高
  • 文本異步渲染

你們能夠看看上面這張圖的對比分析,數據是 iPhone6 的機子用 instruments 抓的,左邊的是用 Auto Layout 繪製界面的數據分析,正常若是想平滑滾動的話,fps 至少須要穩定在 55 左右,咱們能夠發現,在沒有緩存行高和異步渲染的狀況下 fps 是最低的,能夠說是比較卡頓了,至少是能肉眼感受出來,能知足平滑滾動要求的也只有在緩存行高且異步渲染的狀況下;右邊的是沒用 Auto Layout 直接用 frame 來繪製界面的數據分析,能夠發現即便沒有異步渲染,也能勉強知足平滑滾動的要求,若是開啓異步渲染的話,能夠說是至關的絲滑了。github

避免重複屢次計算 cell 行高

TableView 行高計算能夠說是個老生常談的問題了,heightForRowAtIndexPath: 是個調用至關頻繁的方法,在裏面作過多的事情不免會形成卡頓。 在 iOS 8 中,咱們能夠經過設置下面兩個屬性來很輕鬆的實現高度自適應:緩存

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];
}];
}
複製代碼
  • 一個簡單計算 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
複製代碼
  • 行高取值:
- (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 ,下面來說講如何搭配它最大程度知足咱們如絲般順滑的需求:ui

Frame 搭配異步渲染

基本思路和計算 frame 相似,只不過把系統的 boundingRectWithSize:sizeWithAttributes: 換成 YYText 中的方法:atom

  • 配置 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
複製代碼

對比上面 frame,能夠發現多了個 YYTextLayout 屬性,這個屬性能夠提早配置文本的特性,包括 fonttextColor 以及行數、行間距、內間距等等,好處就是能夠把一些邏輯提早處理好,好比根據接口字段,動態配置字體顏色,字號等,若是用 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;
}
複製代碼
  • 賦值:
- (void)setModel:(FrameYYModel *)model {
if (!model) return;
_model = model;

self.titleLabel.frame = model.titleFrame;
self.titleLabel.textLayout = model.titleLayout; //直接取 YYTextLayout
}
複製代碼

Auto Layout 搭配異步渲染

YYText 很是友好,一樣支持 xib,YYText 繼承自 UIView,所要作的事情也很簡單:

  • 在 xib 中配置約束
  • 開啓異步屬性

開啓異步屬性能夠代碼裏設置,也能夠直接在 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 搭配能夠很大程度的提升列表流暢度:

  • 若是時間比較緊迫,能夠直接採起 Auto Layout + UITableView-FDTemplateLayoutCell + YYText 方式

  • 若是列表中文本不包含富文本,僅僅顯示文字,又不想引入這兩個庫,可使用系統方式提早計算 Frame

  • 若是想最大程度的流暢度,就須要提早計算 Frame + YYText,具體你們根據本身狀況選擇合適的方案就行

iOS 中關於列表滾動流暢方案的一些探討

相關文章
相關標籤/搜索