TableView優化之快速滑動下的忽略加載

系列文章:

  • TableView優化之高度緩存功能

  • TableView優化之加載圖片的優化邏輯

  • TableView優化之快速滑動下的忽略加載


最近在搞什麼,所以就順手寫點什麼咯~

這两天一直在搞一個TableView的工具類,因為覺得這個東西寫完可以一勞永逸,所以就去搞了一下,主要是有助於TableView的快捷開發。沒什麼好廢話的了,直接說事吧=。=

在今天的博客中你可能會看到:

  • VVeboTableView中Cell加載邏輯的解析
  • TableView代碼解耦的基本思路

恩,東西不多,一點一點說~


VVeboTableView

其實這是VVebo項目中作者分享剝離的一個Demo,來告訴我們他是怎麼優化TableView的流暢性的。

那麼VVebo是什麼呢?看名字你就猜吧,像不像微博,是的,它就是一款新浪微博的第三方客戶端,當年還是有很多人追捧的,不過後來新浪逐漸收回開發接口導致很多功能無法實現就把VVebo給坑了。

那麼為什麼VVebo使用率那麼高呢?一方面是當時新浪微博客戶端的確不行,另一方面VVebo簡約的風格和流暢的體驗俘獲了一大批用戶。所以今天我們就來探究一下他是如何做到TableView的絲滑體驗的。

首先你可以在這裏現在一份源碼,畢竟源碼面前沒有秘密。

在老司機看來,作者最有效的優化分為4部分:

  • TableViewCell圓角優化
  • 緩存行高
  • 相對固定的圖片及文字採用CoreText繪製
  • TableView加載數據邏輯優化

1.圓角

這部分作者的優化很簡單,他沒有畫圓角!圓角是TableViewCell的幀率殺手大家都知道吧,所以人家根本就沒有畫圓角。他是怎麼做的呢?覆蓋了與背景色同色的圓角圖片,簡單粗暴,果然是個心機boy。

不過關於圓角的優化,還是有更好的解決辦法的,在這裏。不想看的話我給你總結一下,就兩點:

  • 別冤枉cornerRadius,問題不在它。而在於maskToBounds。普通的UIView繪製圓角時並不需要maskToBounds屬性。也就是普通的視圖圓角對卡頓沒有影響。
  • 既然有普通就有特殊:UIImageView和UILabel以及我還沒有發現的=。=對於UIImage的處理建議先藉助CoreGraphic處理圖片吧,直接繪製一個帶圓角的圖片給ImageView吧。對於Label沒有太好的優化方案,是在不行只能CoreText了。其實你會發現,UILable這個控件對中文十!分!不!友!好!很多細節上中文跟英文或者字符會有很大的差異,但是你有不能不用他,好氣哦=。=

2.緩存行高

這部分內容老司機在上一期講述過不定高cell行高緩存的必要性及緩存的方法,這裏不再贅述。


3.CoreText繪製文本

首先,複雜的層級關係同樣會給cell在繪製時添加很大的負擔,這點是毋庸置疑的,所以VVebo的作者選擇了將一些相對重複性很大的視圖選擇使用CoreText和CoreGraphic技術直接繪製在一個視圖上,這樣就減少了視圖的層級,為流暢性又添了一份可能。CoreText繪製文本的和圖片的技術你可以在老司機的CoreText實現圖文混排系列中得到詳細的實現方法,想看的去看吧。


4.TableView加載數據邏輯優化

到現在為止終於要講點之前沒有說過的了=。=

說以下主體思路,VVebo的作者認為,當用戶快速滑動的時候,事實上他對滑動過程中的內容是不關心的,他只關心滾動結束處的內容,那麼用戶不關心的內容她就選擇了不加載。

這是他的主體思路,來看下這部分的實現代碼:

- (void)drawCell:(VVeboTableViewCell *)cell withIndexPath:(NSIndexPath *)indexPath{
    NSDictionary *data = [datas objectAtIndex:indexPath.row];
    cell.selectionStyle = UITableViewCellSelectionStyleNone;
    //清除cell內容,解決復用問題
    [cell clear];
    cell.data = data;
  //判斷如果needLoadArr中含有需要加載的indexPath而當前indexPath又不在其中的時候,則不繪製cell直接返回
    if (needLoadArr.count>0&&[needLoadArr indexOfObject:indexPath]==NSNotFound) {
        [cell clear];
        return;
    }
  //判斷如果scrollToToping為真的時候(及點擊狀態欄快速回到TableView頂部的時候)不繪製cell
    if (scrollToToping) {
        return;
    }
    //上面都沒問題的話,繪製cell
    [cell draw];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    VVeboTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
    if (cell==nil) {
        cell = [[VVeboTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
                                         reuseIdentifier:@"cell"];
    }
    [self drawCell:cell withIndexPath:indexPath];
    return cell;
}

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{
    [needLoadArr removeAllObjects];
}

//按需加載 - 如果目標行與當前行相差超過指定行數,只在目標滾動範圍的前後指定3行加載。
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset{
//取出滾動停止時展示的第一個cell的indexPath
    NSIndexPath *ip = [self indexPathForRowAtPoint:CGPointMake(0, targetContentOffset->y)];
//取出當前展示的第一個cell的indexPath
    NSIndexPath *cip = [[self indexPathsForVisibleRows] firstObject];
    NSInteger skipCount = 8;
//如果兩者之間差距很大則認為滑動速度很快,中間用戶都不關心,直接把滾動停止時的展示的cell加入到needLoadArr數組中
    if (labs(cip.row-ip.row)>skipCount) {
        NSArray *temp = [self indexPathsForRowsInRect:CGRectMake(0, targetContentOffset->y, self.width, self.height)];
        NSMutableArray *arr = [NSMutableArray arrayWithArray:temp];
//根據滾動方向在前或后額外添加三個需要展示的cell,這樣看起來好像更加平滑的樣子
        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];
    }
}

- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView{
    scrollToToping = YES;
    return YES;
}

- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView{
    scrollToToping = NO;
    [self loadContent];
}

- (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView{
    scrollToToping = NO;
    [self loadContent];
}

//用戶觸摸時第一時間加載內容
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    if (!scrollToToping) {
        [needLoadArr removeAllObjects];
        [self loadContent];
    }
    return [super hitTest:point withEvent:event];
}

- (void)loadContent{
    if (scrollToToping) {
        return;
    }
    if (self.indexPathsForVisibleRows.count<=0) {
        return;
    }
    if (self.visibleCells&&self.visibleCells.count>0) {
        for (id temp in [self.visibleCells copy]) {
            VVeboTableViewCell *cell = (VVeboTableViewCell *)temp;
            [cell draw];
        }
    }
}

其實是就100行代碼,思路還是很清晰明了的。作者主要是通過
-drawCell:withIndexPath:這個方法來控制cell的繪製行為的。我們看看他做了什麼?

首先他cell調用了clear方法,這是VVeboTableViewCell中作者自己實現的方法,用於清除cell上面展示的內容,這樣可以避免因cell重用而導致沒有繪製的cell會显示之前的內容的問題。然後是判斷needLoadArr中是否包含有當前indexPath,若沒有返回。繼續判斷當前TableView是否處於快速回到頂部的過程中,如果是的話也不繪製。最後上述條件都滿足的時候再進行cell的繪製

所以重點來了,needLoadArr什麼時候添加的元素?如何獲取到TableView快速回到頂部的時間點?

第二點好說,點擊狀態欄的時候,TableView會詢問代理 - scrollViewShouldScrollToTop:只有返回YES的時候才會快速回到頂部,這時我們可以在這捕獲到這個狀態。但是可以看到作者並沒有在這選擇添加頂部可能要展示的cell進needLoadArr數組,那麼當他滾動到頂部的時候我們要將頂部的cell進行直接更新,所以通過- scrollViewDidEndScrollingAnimation:- scrollViewShouldScrollToTop:兩個代理拿到到達頂部的狀態后直接更新當前cell。

回過頭來我們說下第一點,needLoadArr是怎麼操作呢?
我們知道我們是要判斷TableView快速滑動,那我們怎麼拿到這個行為呢?要知道沒有什麼代理是直接反應滾動速度的,這裏作者很取巧的用到了-scrollViewWillEndDragging:withVelocity:targetContentOffset:這個代理。
這個代理在手指即將結束拖動的時候出發,他會告訴外界當前的速度及這次會滾動到的位置。

所以作者在這裏判斷了目標位置與當前位置相差間隔,如果很大的話則認為中間內容不需加載,直接添加目標位置的內容進入數組。

恩,以上就是VVebo作者對數據加載邏輯的優化。

這是依靠着上述四點,VVebo才獲得了完美的滑動體驗,其思路也是我們開發中可以學習和借鑒的


TableView解耦

這部分內容也不是什麼新鮮事,也是比較靠譜的一個思路。當然了這部分內容不是對性能的優化,而是對代碼的優化。

天天寫TableView裏面的代理是不是很煩人啊,千篇一律又不能不寫。所以想一個方法只寫一次以後拿來直接用吧=。=


效果圖

真機不卡!真機不卡!真機不卡!重要的事情說三遍

放一個效果圖,老司機寫的控制器裏面看不到任何一個TableView代理然而還是能正常显示並實現很多功能。

但是代碼怎麼可能不寫,只是我在別的地方寫過了,並且花了大把時間進行解耦,讓每一個TableView都能拿來就直接使用。

那麼這個解耦的類我們要怎麼寫呢?

好的,我們來新建一個文件。


helper類

這個類只需要一個屬性,是一個數組。就是你平常寫TableView的時候的數據源。

然後在.m中我們就可以像平常寫TableView一樣在這裏面寫代理了。


假裝寫了兩個代理

無視我的cell和model,嫌累沒創建=。=

最後在VC中把TableView的dataSource設成Helper就好了。


無視我這代碼,我就是給你展現個邏輯,細寫嫌累

重點是別忘了持有helper類。tableView對dataSource是弱引用,如果不持有helper就被釋放了。
就是這麼一個思路。的確該寫你都寫了,不過好處就是你以後把helper類拿到另一個工程還可以直接用。

恩,思路就是這麼簡單的一個思路,不過你可以把你的helper類寫的功能更加豐富一些。比如說我的helper類。老司機添加了高度緩存、滾動優化等優化功能,並且對選擇、展示動畫、無數據佔位圖等常用功能都進行了支持。而且老司機也在不斷的豐富helper類的功能。

只放一個版本更新記錄吧,代碼放不下=。=

/**
DWTableViewHelper
TableView工具類
抽出TableView代理,減小VC壓力,添加常用代理映射

version 1.0.0
添加常用代理映射
添加helper基礎屬性

version 1.0.1
去除註冊,改為更適用的重用模式

version 1.0.2
添加多分組模式

version 1.0.3
添加選擇模式及相關api

version 1.0.4
添加helper設置cell類型及復用標識

version 1.0.5
將cell的基礎屬性提出協議,helper與model同時遵守協議

version 1.0.6
修正佔位視圖展示時機,提供兩個刷新列表擴展方法,提供展示、隱藏佔位圖接口

version 1.0.7
添加選則模式下單選多選控制

version 1.0.8
補充組頭視圖、尾視圖行高代理映射並簡化代理鏈

version 1.0.9
cell基類添加父類實現強制調用宏、斷言中給出未能加載的cell類名

version 1.1.0
改變cell劃線機制,改為系統分割線,添加分割線歸0方法
添加自動行高計算並緩存
cell添加xib支持
修複選擇模式選中后關閉再次開啟選擇同一個無法選中bug
更換去除選擇背景方式,解決與選擇模式的衝突
映射所有代理

version 1.1.1
添加自適應模式最小行高限制及最大行高設置
添加數據源的容錯機制,但這並不是你故意寫錯的理由=。=
添加屏幕判斷,當位置方向時,默認返回豎屏
額外補充動畫代理、支持CAAnimation及DWAnimation

version 1.1.2
展示動畫邏輯修改,DWAnimation動畫展示方法替換

version 1.1.3
滾動優化模式添加
高速忽略模式完成
懶加載模式完成
懶加載模式動畫隱藏,更加平滑,修復刷新bug。
有沒有美工妹子給切幾張佔位圖。。我做的圖太丑了。。

*/

是的,所以說你玩去那可以寫一個什麼都能做的Helper

正如我最開始的效果圖。如果你想看看我還對Helper做了什麼你可以去我的倉庫上面看DWTableViewHelper

你想直接用也可以,你可以去GitHub上面直接托一份,也可以用cocoaPods集成:

      pod 'DWTableViewHelper', '~> 1.1.2'

DWTableViewHelper類當前為1.1.2版本,滾動優化在1.1.3版本pod還沒有發,因為在測試看有沒有什麼bug,而且老司機做的圖有的丑,急需會美工的妹子幫我切兩張圖,漢子也行,願意幫忙的私信我=。=

如果你想看看老司機的所有pods項目的話,你也可以打開終端,輸入

pod search wicky

pod search wicky

最後,雙擊666,加波關注,點波star,老鐵沒毛病!


老鐵沒毛病


分享