UITableView 性能優(yōu)化

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)控制是否顯示

參考文章:

  1. iOS 保持界面流暢的技巧
  2. VVeboTableViewDemo
  3. UITableView優(yōu)化技巧
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容