UITableView性能優化-中級篇

老實說,UITableView性能優化 這個話題,最經常遇到的還是在面試中,常見的回答例如:

  • Cell復用機制
  • Cell高度預先計算
  • 緩存Cell高度
  • 圓角切割
  • 等等. . .
made in 小蠢驢的配圖

進階篇

最近遇到一個需求,對tableView有中級優化需求

  1. 要求 tableView 滾動的時候,滾動到哪行,哪行的圖片才加載并顯示,滾動過程中圖片不加載顯示;
  2. 頁面跳轉的時候,取消當前頁面的圖片加載請求;

以最常見的cell加載webImage為例:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
    if (!cell) {
        cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
    }
    
    DemoModel *model = self.datas[indexPath.row];
    cell.textLabel.text = model.text;
   
    [cell.imageView setYy_imageURL:[NSURL URLWithString:model.user.avatar_large]];
    
    return cell;
}
解釋下cell的復用機制:
  • 如果cell沒進入到界面中(還不可見),不會調用- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath去渲染cell,在cell中如果設置loadImage,不會調用;
  • 而當cell進去界面中的時候,再進行cell渲染(無論是init還是從復用池中取)

解釋下YYWebImage機制:
  • 內部的YYCache會對圖片進行數據緩存,以key:value的形式,這里的key = imageUrlvalue = 下載的image圖片
  • 讀取的時候判斷YYCache中是否有該url,有的話,直接讀取緩存圖片數據,沒有的話,走圖片下載邏輯,并緩存圖片

問題所在:

如上設置,如果我們cell一行有20行,頁面啟動的時候,直接滑動到最底部,20個cell都進入過了界面,- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 被調用了20次,不符合 需求1的要求

解決辦法:

  1. cell每次被渲染時,判斷當前tableView是否處于滾動狀態,是的話,不加載圖片;
  2. cell 滾動結束的時候,獲取當前界面內可見的所有cell
  3. 2的基礎之上,讓所有的cell請求圖片數據,并顯示出來
  • 步驟1:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
    if (!cell) {
        cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
    }
    
    DemoModel *model = self.datas[indexPath.row];
    cell.textLabel.text = model.text;
   
    //不在直接讓cell.imageView loadYYWebImage
    if (model.iconImage) {
        cell.imageView.image = model.iconImage;
    }else{
        cell.imageView.image = [UIImage imageNamed:@"placeholder"];
        
        //核心判斷:tableView非滾動狀態下,才進行圖片下載并渲染
        if (!tableView.dragging && !tableView.decelerating) {
            //下載圖片數據 - 并緩存
            [ImageDownload loadImageWithModel:model success:^{
                
                //主線程刷新UI
                dispatch_async(dispatch_get_main_queue(), ^{
                    cell.imageView.image = model.iconImage;
                });
            }];
        }
}
  • 步驟2:
- (void)p_loadImage{

    //拿到界面內-所有的cell的indexpath
    NSArray *visableCellIndexPaths = self.tableView.indexPathsForVisibleRows;

    for (NSIndexPath *indexPath in visableCellIndexPaths) {

        DemoModel *model = self.datas[indexPath.row];

        if (model.iconImage) {
            continue;
        }

        UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];

        [ImageDownload loadImageWithModel:model success:^{
            //主線程刷新UI
            dispatch_async(dispatch_get_main_queue(), ^{
 
                cell.imageView.image = model.iconImage;
            });
        }];
    }
}
  • 步驟3:
//手一直在拖拽控件
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{

    [self p_loadImage];
}

//手放開了-使用慣性-產生的動畫效果
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{

    if(!decelerate){
        //直接停止-無動畫
        [self p_loadImage];
    }else{
        //有慣性的-會走`scrollViewDidEndDecelerating`方法,這里不用設置
    }
}

dragging:returns YES if user has started scrolling. this may require some time and or distance to move to initiate dragging
可以理解為,用戶在拖拽當前視圖滾動(手一直拉著)

deceleratingreturns:returns YES if user isn't dragging (touch up) but scroll view is still moving
可以理解為用戶手已放開,試圖是否還在滾動(是否慣性效果)

ScrollView一次拖拽的代理方法執行流程:
ScrollView流程圖.png

當前代碼生效的效果如下:


demo.gif
RunLoop小操作
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
    if (!cell) {
        cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
    }
    
    DemoModel *model = self.datas[indexPath.row];
    cell.textLabel.text = model.text;
   
    
    if (model.iconImage) {
        cell.imageView.image = model.iconImage;
    }else{
        cell.imageView.image = [UIImage imageNamed:@"placeholder"];

        /**
         runloop - 滾動時候 - trackingMode,
         - 默認情況 - defaultRunLoopMode
         ==> 滾動的時候,進入`trackingMode`,defaultMode下的任務會暫停
         停止滾動的時候 - 進入`defaultMode` - 繼續執行`trackingMode`下的任務 - 例如這里的loadImage
         */
        [self performSelector:@selector(p_loadImgeWithIndexPath:)
                   withObject:indexPath
                   afterDelay:0.0
                      inModes:@[NSDefaultRunLoopMode]];
}

//下載圖片,并渲染到cell上顯示
- (void)p_loadImgeWithIndexPath:(NSIndexPath *)indexPath{
    
    DemoModel *model = self.datas[indexPath.row];
    UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
    
    [ImageDownload loadImageWithModel:model success:^{
        //主線程刷新UI
        dispatch_async(dispatch_get_main_queue(), ^{
            cell.imageView.image = model.iconImage;
        });
    }];
}

效果與demo.gif的效果一致

runloop - 兩種常用模式介紹: trackingMode && defaultRunLoopMode

  • 默認情況 - defaultRunLoopMode
  • 滾動時候 - trackingMode
  • 滾動的時候,進入trackingMode,導致defaultMode下的任務會被暫停,停止滾動的時候 ==> 進入defaultMode - 繼續執行defaultMode下的任務 - 例如這里的defaultMode

大tips:這里,如果使用RunLoop,滾動的時候雖然不執行defaultMode,但是滾動一結束,之前cell中的p_loadImgeWithIndexPath就會全部再被調用,導致類似YYWebImage的效果,其實也是不滿足需求,

  • 提示會被調用的代碼如下:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    
    //p_loadImgeWithIndexPath一進入`NSDefaultRunLoopMode`就會執行
    [self performSelector:@selector(p_loadImgeWithIndexPath:)
               withObject:indexPath
               afterDelay:0.0
                  inModes:@[NSDefaultRunLoopMode]];
}
runloopDemo.gif

效果如上

  • 滾動的時候不加載圖片,滾動結束加載圖片-滿足
  • 滾動結束,之前滾動過程中的cell會加載圖片 => 不滿足需求


版本回滾到Runloop之前 - git reset --hard runloop之前

解決: 需求2. 頁面跳轉的時候,取消當前頁面的圖片加載請求;


- (void)p_loadImgeWithIndexPath:(NSIndexPath *)indexPath{
    
    DemoModel *model = self.datas[indexPath.row];
    
    //保存當前正在下載的操作
    ImageDownload *manager = self.imageLoadDic[indexPath];
    if (!manager) {
        
        manager = [ImageDownload new];
        //開始加載-保存到當前下載操作字典中
        [self.imageLoadDic setObject:manager forKey:indexPath];
    }
    
    [manager loadImageWithModel:model success:^{
        //主線程刷新UI
        dispatch_async(dispatch_get_main_queue(), ^{
            UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
            cell.imageView.image = model.iconImage;
        });
        
        //加載成功-從保存的當前下載操作字典中移除
        [self.imageLoadDic removeObjectForKey:indexPath];
    }];
}

- (void)viewWillDisappear:(BOOL)animated{
    [super viewWillDisappear:animated];
  
    NSArray *loadImageManagers = [self.imageLoadDic allValues];
    //當前圖片下載操作全部取消
    [loadImageManagers makeObjectsPerformSelector:@selector(cancelLoadImage)];
}


@implementation ImageDownload
- (void)cancelLoadImage{
    [_task cancel];
}
@end

思路:

  1. 創建一個可變字典,以indexPath:manager的格式,將當前的圖片下載操作存起來
  2. 每次下載之前,將當前下載線程存入,下載成功后,將該線程移除
  3. viewWillDisappear的時候,取出當前線程字典中的所有線程對象,遍歷進行cancel操作,完成需求


話外篇:面試題贈送

最近網上各種互聯網公司裁員信息鋪天蓋地,甚至包括各種一線公司 ( X東 X乎 都扛不住了嗎-。-)iOS本來就是提前進入寒冬,iOS小白們可以嘗試思考下這個問題

問:UITableView的圓角性能優化如何實現

答:

  1. 讓服務器直接傳圓角圖片;
  2. 貝塞爾切割控件layer;
  3. YYWebImage為例,可以先下載圖片,再對圖片進行圓角處理,再設置到cell上顯示
問:YYWebImage 如何設置圓角? 在下載完成的回調中?如果你在下載完成的時候再切割,此時 YYWebImage 緩存中的圖片是初始圖片,還是圓角圖片?(終于等到3了!!)

答: 如果是下載完,在回調中進行切割圓角的處理,其實緩存的圖片是原圖,等于每次取的時候,緩存中取出來的都是矩形圖片,每次set都得做切割操作;

問: 那是否有解決辦法?

答:其實是有的,簡單來說YYWebImage 可以拆分成兩部分,默認情況下,我們拿到的回調,是走了 download && cache的流程了,這里我們多做一步,取出cache中該url路徑對應的圖片,進行圓角切割,再存儲到 cache中,就能保證以后每次拿到的就都是cacha中已經裁切好的圓角圖片

詳情可見:

NSString *path = [[UIApplication sharedApplication].cachesPath stringByAppendingPathComponent:@"weibo.avatar"];
YYImageCache *cache = [[YYImageCache alloc] initWithPath:path];
manager = [[YYWebImageManager alloc] initWithCache:cache queue:[YYWebImageManager sharedManager].queue];
manager.sharedTransformBlock = ^(UIImage *image, NSURL *url) {
    if (!image) return image;
    return [image imageByRoundCornerRadius:100]; // a large value
};

SDWebImage同理,它有暴露了一個方法出來,可以直接設置保存圖片到磁盤中,無需修改源碼

“winner is coming”,如果面試正好遇到以上問題的,請叫我雷鋒~
衷心希望各位iOS小伙伴門能熬過這個冬天?


Demo源碼


參考資料
iOS 保持界面流暢的技巧
VVeboTableViewDemo
YYKitDemo
UIScrollView 實踐經驗

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Swift1> Swift和OC的區別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴謹 對...
    cosWriter閱讀 11,135評論 1 32
  • 五年級英語作業本上的作業 1、英語書的第7頁的第九部分抄寫兩次。 2、英語書第三頁的1b部分抄寫兩次。 3、英語書...
    我心我愿秀閱讀 922評論 0 1
  • 邪生 秋智是純樸農民的兒子,他的家中有五個孩子,倆個姐姐,倆個哥哥,唯獨他是最小的那一個,秋智的爸爸看到自己的一雙...
    Mr海鮮君的故事閱讀 518評論 1 3
  • 一個人如果身體生病了我們會說他身體不健康,那么就需要去醫院打針吃藥,盡快治愈病癥。可如果一個人感覺不到幸福呢?是不...
    耕耘生活閱讀 397評論 4 10
  • 我們的生命不是一個人的! 早上突然接到親人去世的噩耗,心理好是難受一陣。 離去的是我的姐夫,年僅四十出頭,家里還有...
    飛躍銀河閱讀 512評論 3 0