UITableView 性能優(yōu)化
最近在閱讀 ibireme 文章時(shí),YY 在異步繪制中提到了 VVeboTableViewDemo,該項(xiàng)目提供了一種滑動(dòng)過(guò)程中 Cell 繪制性能提升的解決方案。本文在介紹 VVeboTableViewDemo 實(shí)現(xiàn)細(xì)節(jié)的同時(shí)會(huì)對(duì)現(xiàn)有 UITableView
已有優(yōu)化方案進(jìn)行介紹并分析其中優(yōu)缺點(diǎn)。
UITableView 簡(jiǎn)介
UITableView 和其他控件相比最大的特點(diǎn)是:UITableViewCell 的重用機(jī)制。簡(jiǎn)而言之:UITableView 只會(huì)創(chuàng)建一屏(或一屏多點(diǎn))的 UITableViewCell,每次顯示時(shí)都是復(fù)用這些 Cell。當(dāng)要顯示某一位置的 Cell 時(shí),會(huì)先去集合(或數(shù)組)中取,如果有,直接顯示;如沒(méi)有,創(chuàng)建 Cell 并放入緩存池。當(dāng) Cell 滑出屏幕時(shí),該 Cell 就會(huì)被放入集合(或數(shù)組)中。
UITableView 在顯示時(shí)會(huì)多次調(diào)用這兩個(gè)方法:
- tableView:heightForRowAtIndexPath:
- tableView:cellForRowAtIndexPath:
通常情況下,我們會(huì)認(rèn)為 UITableView 在顯示的時(shí)候會(huì)先調(diào)用前者,再調(diào)用后者,這和我們創(chuàng)建控件的思路是一致的,先創(chuàng)建它,再設(shè)置布局。但實(shí)際使用時(shí)并非如此,UITableView 是繼承自 UIScrollView,需要先確定 contentSize 及每個(gè) Cell 的位置,然后才會(huì)把復(fù)用的 Cell 放到對(duì)應(yīng)的位置。因此,UITableView 會(huì)先多次回調(diào) - tableView:heightForRowAtIndexPath:
確定 contentSize 和 Cell 的位置,然后再調(diào)用 - tableView:cellForRowAtIndexPath:
來(lái)確定顯示的 Cell。
舉個(gè)例子:
現(xiàn)在要顯示100個(gè) Cell,一屏顯示5個(gè),那么刷新(reload)TableView 時(shí),TableView 會(huì)先調(diào)用100次 - tableView:heightForRowAtIndexPath:
方法,然后調(diào)用5次 - tableView:cellForRowAtIndexPath:
方法;滑動(dòng)屏幕時(shí),當(dāng)有新 Cell 滑入屏幕時(shí),都會(huì)調(diào)用一次- tableView:heightForRowAtIndexPath:
、- tableView:cellForRowAtIndexPath:
方法。
UITableView 優(yōu)化
上一節(jié)已對(duì) TableView 的復(fù)用機(jī)制和核心方法進(jìn)行了簡(jiǎn)要介紹,下面將基于示例來(lái)介紹現(xiàn)有 TableView 的優(yōu)化方案。
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
ContacterTableCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ContacterTableCell"];
if (!cell) {
cell = (ContacterTableCell *)[[[NSBundle mainBundle] loadNibNamed:@"ContacterTableCell" owner:self options:nil] lastObject];
}
NSDictionary *dict = self.dataList[indexPath.row];
[cell setContentInfo:dict];
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
return cell.frame.size.height;
}
上面代碼是我們初次使用 TableView 時(shí)的常見(jiàn)寫(xiě)法,很多教程也是這么來(lái)寫(xiě)的。但基于上一節(jié)中的分析我們知道,在顯示一屏 Cell 之前,我們需要計(jì)算全部 Cell 的高度。如果有1000行數(shù)據(jù),就會(huì)調(diào)用1000+次 - cellForRowAtIndexPath:indexPath
,而該方法非常重,我們會(huì)在該方法中對(duì)模型賦值,設(shè)置 Cell 布局等,每次調(diào)用開(kāi)銷(xiāo)很大,滑動(dòng)過(guò)程中會(huì)卡頓,急需優(yōu)化。
預(yù)計(jì)算高度并緩存
例子中代碼存在兩個(gè)問(wèn)題:
- 高度計(jì)算和 Cell 賦值耦合
- 高度未緩存
高度計(jì)算和 Cell 賦值應(yīng)當(dāng)分離,TableView 的兩個(gè)回調(diào)方法應(yīng)各司其職,不應(yīng)存在依賴關(guān)系。Cell 的高度計(jì)算過(guò)后就不會(huì)變更,此時(shí)可以將其緩存,下次使用時(shí)直接讀取即可。
基于上述思路,從網(wǎng)絡(luò)獲取到數(shù)據(jù)后,根據(jù)數(shù)據(jù)計(jì)算出相應(yīng)的布局,并緩存到數(shù)據(jù)源中,在 - tableView:heightForRowAtIndexPath:
方法中可直接返回高度,不需要重復(fù)計(jì)算。
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
NSDictionary *dict = self.dataList[indexPath.row];
CGRect rect = [dict[@"frame"] CGRectValue];
return rect.frame.height;
}
本方案在一般的場(chǎng)景下可以滿足性能的要求,但是在像朋友圈圖文混排需求面前,依舊會(huì)有卡頓現(xiàn)象出現(xiàn)。究其原因:本方案中所有 Cell 的繪制都放在主線程,當(dāng) Cell 非常復(fù)雜主線程繪制不及時(shí)就會(huì)出現(xiàn)卡頓。
異步繪制
上一個(gè)方案中所有 Cell 的繪制都在主線程中進(jìn)行,如將繪制任務(wù)提交到后臺(tái)線程,則主線程任務(wù)會(huì)顯著減少,滑動(dòng)性能會(huì)顯著提升。
首先為自定義的 Cell 添加 draw
方法,在方法體中實(shí)現(xiàn)繪制任務(wù):
// 異步繪制
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
CGRect rect = [_data[@"frame"] CGRectValue];
UIGraphicsBeginImageContextWithOptions(rect.size, YES, 0);
CGContextRef context = UIGraphicsGetCurrentContext();
// 整個(gè)內(nèi)容的背景
[[UIColor colorWithRed:250/255.0 green:250/255.0 blue:250/255.0 alpha:1] set];
CGContextFillRect(context, rect);
// 轉(zhuǎn)發(fā)內(nèi)容的背景
if ([_data valueForKey:@"subData"]) {
[[UIColor colorWithRed:243/255.0 green:243/255.0 blue:243/255.0 alpha:1] set];
CGRect subFrame = [_data[@"subData"][@"frame"] CGRectValue];
CGContextFillRect(context, subFrame);
[[UIColor colorWithRed:200/255.0 green:200/255.0 blue:200/255.0 alpha:1] set];
CGContextFillRect(context, CGRectMake(0, subFrame.origin.y, rect.size.width, .5));
}
{
// 名字
float leftX = SIZE_GAP_LEFT+SIZE_AVATAR+SIZE_GAP_BIG;
float x = leftX;
float y = (SIZE_AVATAR-(SIZE_FONT_NAME+SIZE_FONT_SUBTITLE+6))/2-2+SIZE_GAP_TOP+SIZE_GAP_SMALL-5;
[_data[@"name"] drawInContext:context withPosition:CGPointMake(x, y) andFont:FontWithSize(SIZE_FONT_NAME)
andTextColor:[UIColor colorWithRed:106/255.0 green:140/255.0 blue:181/255.0 alpha:1]
andHeight:rect.size.height];
// 時(shí)間+設(shè)備
y += SIZE_FONT_NAME+5;
float fromX = leftX;
float size = [UIScreen screenWidth]-leftX;
NSString *from = [NSString stringWithFormat:@"%@ %@", _data[@"time"], _data[@"from"]];
[from drawInContext:context withPosition:CGPointMake(fromX, y) andFont:FontWithSize(SIZE_FONT_SUBTITLE)
andTextColor:[UIColor colorWithRed:178/255.0 green:178/255.0 blue:178/255.0 alpha:1]
andHeight:rect.size.height andWidth:size];
}
// 將繪制的內(nèi)容以圖片的形式返回,并調(diào)主線程顯示
UIImage *temp = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
dispatch_async(dispatch_get_main_queue(), ^{
if (flag==drawColorFlag) {
postBGView.frame = rect;
postBGView.image = nil;
postBGView.image = temp;
}
}
// 繪制文本
[self drawText];
}}
Cell 中各項(xiàng)內(nèi)容都根據(jù)之前算好的布局進(jìn)行異步繪制,此時(shí) TableView 的性能較之前又提高了一個(gè)等級(jí)。
條件繪制
但 TableView 的優(yōu)化之路仍未停止,在 TableView 高速滑動(dòng)時(shí),滑動(dòng)過(guò)程中的多數(shù) Cell 對(duì)用戶來(lái)說(shuō)都是無(wú)用的,用戶只關(guān)心滑動(dòng)停止時(shí)附近的幾個(gè) Cell?;瑒?dòng)時(shí),用戶松開(kāi)手指后,立刻計(jì)算出滑動(dòng)停止時(shí) Cell 的位置,并預(yù)先繪制那個(gè)位置附近的幾個(gè) Cell。這個(gè)方法比較有技巧性,并且對(duì)滑動(dòng)性能來(lái)說(shuō)提升巨大,唯一的缺點(diǎn)就是快速滑動(dòng)中會(huì)出現(xiàn)大量空白內(nèi)容。
//按需加載 - 如果目標(biāo)行與當(dāng)前行相差超過(guò)指定行數(shù),只在目標(biāo)滾動(dòng)范圍的前后指定3行加載。
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset{
NSIndexPath *ip = [self indexPathForRowAtPoint:CGPointMake(0, targetContentOffset->y)];
NSIndexPath *cip = [[self indexPathsForVisibleRows] firstObject];
NSInteger skipCount = 8;
if (labs(cip.row-ip.row)>skipCount) {
NSArray *temp = [self indexPathsForRowsInRect:CGRectMake(0, targetContentOffset->y, self.width, self.height)];
NSMutableArray *arr = [NSMutableArray arrayWithArray:temp];
if (velocity.y < 0) { // 下拉
NSIndexPath *indexPath = [temp lastObject];
if (indexPath.row+3<datas.count) {
[arr addObject:[NSIndexPath indexPathForRow:indexPath.row+1 inSection:0]];
[arr addObject:[NSIndexPath indexPathForRow:indexPath.row+2 inSection:0]];
[arr addObject:[NSIndexPath indexPathForRow:indexPath.row+3 inSection:0]];
}
} else { // 上滑
NSIndexPath *indexPath = [temp firstObject];
if (indexPath.row>3) {
[arr addObject:[NSIndexPath indexPathForRow:indexPath.row-3 inSection:0]];
[arr addObject:[NSIndexPath indexPathForRow:indexPath.row-2 inSection:0]];
[arr addObject:[NSIndexPath indexPathForRow:indexPath.row-1 inSection:0]];
}
}
[needLoadArr addObjectsFromArray:arr];
}
}
在 - tableView:cellForRowAtIndexPath:
中加入判斷:
if (needLoadArr.count>0&&[needLoadArr indexOfObject:indexPath]==NSNotFound) {
[cell clear];
return;
}
快速滑動(dòng)時(shí),只加載目標(biāo)區(qū)域的 Cell,按需繪制,提高 TableView 流暢度。
總結(jié)
通過(guò)介紹上述幾個(gè)優(yōu)化方案,TableView 的優(yōu)化可以從下面幾方面入手:
- 提前計(jì)算并緩存高度
- 異步渲染內(nèi)容到圖片
- 滑動(dòng)時(shí)按需加載
除了上述大方向外,TableView 還有很多大家都熟知的優(yōu)化點(diǎn):
- 正確使用 reuseIdentifier 來(lái)重用Cells
- 盡量使所有的 view opaque,包括Cell自身
- 盡量少用或不用透明圖層
- 如果 Cell 內(nèi)現(xiàn)實(shí)的內(nèi)容來(lái)自 web,使用異步加載,緩存請(qǐng)求結(jié)果
- 減少 subviews 的數(shù)量
- 在heightForRowAtIndexPath:中盡量不使用 cellForRowAtIndexPath:,如果你需要用到它,只用一次然后緩存結(jié)果
- 盡量少用 addView 給 Cell 動(dòng)態(tài)添加 View,可以初始化時(shí)就添加,然后通過(guò)hide來(lái)控制是否顯示
參考文章: