久久国产成人av_抖音国产毛片_a片网站免费观看_A片无码播放手机在线观看,色五月在线观看,亚洲精品m在线观看,女人自慰的免费网址,悠悠在线观看精品视频,一级日本片免费的,亚洲精品久,国产精品成人久久久久久久

分享

優(yōu)化UITableViewCell高度計算的那些事

 quasiceo 2015-06-04

優(yōu)化UITableViewCell高度計算的那些事

發(fā)表于2015-05-19 13:46| 1039次閱讀| 來源Sunny'Blog| 3 條評論| 作者孫源

摘要:在將UITableView+FDTemplateLayoutCell自動算高工具更新至1.2版本之后,,百度知道iOS團隊對UITableViewCell利用AutoLayout自動高度計算和UITableView滑動優(yōu)化進行了總結(jié),以及RunLoop實踐技巧,。

前言

這篇文章是我和我們團隊最近對UITableViewCell利用AutoLayout自動高度計算和UITableView滑動優(yōu)化的一個總結(jié),。從這篇文章里,你可以讀到:

  • UITableView高度計算和估算的機制
  • 不同iOS系統(tǒng)在高度計算上的差異
  • iOS8 self-sizing cell
  • UITableView+FDTemplateLayoutCell如何用一句話解決高度問題
  • UITableView+FDTemplateLayoutCell中對RunLoop的使用技巧

UITableViewCell高度計算

rowHeight

UITableView是我們再熟悉不過的視圖了,,它的delegate和data source回調(diào)不知寫了多少次,,也不免遇到UITableViewCell高度計算的事。UITableView詢問cell高度有兩種方式,。一種是針對所有Cell具有固定高度的情況,,通過:

self.tableView.rowHeight = 88;

上面的代碼指定了一個所有cell都是88高度的UITableView,對于定高需求的表格,,強烈建議使用這種(而非下面的)方式保證不必要的高度計算和調(diào)用,。rowHeight屬性的默認值是44,所以一個空的UITableView顯示成那個樣子,。

另一種方式就是實現(xiàn)UITableViewDelegate中的:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    // return xxx
}

需要注意的是,,實現(xiàn)了這個方法后,rowHeight的設(shè)置將無效,。所以,,這個方法適用于具有多種cell高度的UITableView。

estimatedRowHeight

這個屬性iOS7就出現(xiàn)了,文檔是這么描述它的作用的:

If the table contains variable height rows, it might be expensive to calculate all their heights when the table loads. Using estimation allows you to defer some of the cost of geometry calculation from load time to scrolling time.

恩,,聽上去蠻靠譜的,。我們知道,UITableView是個UIScrollView,,就像平時使用UIScrollView一樣,,加載時指定contentSize后它才能根據(jù)自己的bounds、contentInset,、contentOffset等屬性共同決定是否可以滑動以及滾動條的長度,。而UITableView在一開始并不知道自己會被填充多少內(nèi)容,于是詢問data source個數(shù)和創(chuàng)建cell,,同時詢問delegate這些cell應(yīng)該顯示的高度,,這就造成它在加載的時候浪費了多余的計算在屏幕外邊的cell上。和上面的rowHeight很類似,,設(shè)置這個估算高度有兩種方法:

self.tableView.estimatedRowHeight = 88;
// or
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
    // return xxx
}

有所不同的是,,即使面對種類不同的cell,我們依然可以使用簡單的estimatedRowHeight屬性賦值,,只要整體估算值接近就可以,,比如大概有一半cell高度是44, 一半cell高度是88,, 那就可以估算一個66,,基本符合預(yù)期。

說完了估算高度的基本使用,,可以開始吐槽了:

  1. 設(shè)置估算高度后,,contentSize.height根據(jù)“cell估算值 x cell個數(shù)”計算,這就導致滾動條的大小處于不穩(wěn)定的狀態(tài),,contentSize會隨著滾動從估算高度慢慢替換成真實高度,,肉眼可見滾動條突然變化甚至“跳躍”。
  2. 若是有設(shè)計不好的下拉刷新或上拉加載控件,,或是KVO了contentSize或contentOffset屬性,,有可能使表格滑動時跳動。
  3. 估算高度設(shè)計初衷是好的,,讓加載速度更快,,那憑啥要去侵害滑動的流暢性呢,用戶可能對進入頁面時多零點幾秒加載時間感覺不大,,但是滑動時實時計算高度帶來的卡頓是明顯能體驗到的,,個人覺得還不如一開始都算好了呢(iOS 8更過分,即使都算好了也會邊劃邊計算),。

iOS8 self-sizing cell

具有動態(tài)高度內(nèi)容的cell一直是個頭疼的問題,,比如聊天氣泡的cell,,frame布局時代通常是用數(shù)據(jù)內(nèi)容反算高度:

CGFloat height = textHeightWithFont() + imageHeight + topMargin + bottomMargin + ...;

供UITableViewDelegate調(diào)用時很可能是個cell的類方法:

@interface BubbleCell : UITableViewCell
+ (CGFloat)heightWithEntity:(id)entity;
@end

各種魔法margin加上耦合了屏幕寬度。

AutoLayout時代好了不少,,提供了-systemLayoutSizeFittingSize:的API,,在contentView中設(shè)置約束后,就能計算出準確的值,;缺點是計算速度肯定沒有手算快,,而且這是個實例方法,需要維護專門為計算高度而生的template layout cell,,它還要求使用者對約束設(shè)置的比較熟練,,要保證contentView內(nèi)部上下左右所有方向都有約束支撐,設(shè)置不合理的話計算的高度就成了0,。

這里還不得不提到一個UILabel的蛋疼問題,,當UILabel行數(shù)大于0時,需要指定preferredMaxLayoutWidth后它才知道自己什么時候該折行,。這是個“雞生蛋蛋生雞”的問題,,因為UILabel需要知道superview的寬度才能折行,而superview的寬度還依仗著子view寬度的累加才能確定,。這個問題好像到iOS 8才能夠自動解決(不過我們找到了解決方案),。

回到正題,,iOS 8 WWDC中推出了self-sizing cell的概念,,旨在讓cell自己負責自己的高度計算,使用frame layout和auto layout都可以享受到:


這個特性首先要求是iOS 8,,要是最低支持的系統(tǒng)版本小于8的話,,還得針對老版本單寫套老式的算高(囧),不過用的API到不是新面孔:

self.tableView.estimatedRowHeight = 213;
self.tableView.rowHeight = UITableViewAutomaticDimension;

這里又不得不吐槽了,,自動計算rowHeight跟estimatedRowHeight到底是有什么仇,,如果不加上估算高度的設(shè)置,自動算高就失效了,。

PS:iOS 8系統(tǒng)中rowHeight的默認值已經(jīng)設(shè)置成了UITableViewAutomaticDimension,,所以第二行代碼可以省略。

問題:

  • 這個自動算高在push到下一個頁面或者轉(zhuǎn)屏時會出現(xiàn)高度特別詭異的情況,,不過現(xiàn)在的版本修復了,。
  • 求一個能讓最低支持iOS 8的公司。

iOS 8抽風的算高機制

相同的代碼在iOS 7和iOS 8上滑動順暢程度完全不同,,iOS 8莫名奇妙的卡,。很大一部分原因是iOS 8上的算高機制大不相同,這是我做的小測試:


研究后發(fā)現(xiàn)這么多次額外計算有下面的原因:

  1. 不開啟高度估算時,,UITableView上來就要對所有cell調(diào)用算高來確定contentSize,。
  2. dequeueReusableCellWithIdentifier:forIndexPath: 相比不帶“forIndexPath”的版本會多調(diào)用一次高度計算,。
  3. iOS 7計算高度后有“緩存”機制,不會重復計算,;而iOS 8不論何時都會重新計算cell高度,。

iOS 8把高度計算搞成這個樣子,從WWDC也倒是能找到點解釋,,cell被認為隨時都可能改變高度(如從設(shè)置中調(diào)整動態(tài)字體大?。悦看位瑒映鰜砗蠖家匦掠嬎愀叨?。

說了這么多,,究竟有沒有既能省去算高煩惱,又能保證順暢的滑動,,還能支持iOS 6+的一站式解決方案呢,?

UITableView+FDTemplateLayoutCell

使用UITableView+FDTemplateLayoutCell無疑是解決算高問題的最佳實踐之一,既有iOS 8 self-sizing功能簡單的API,,又可以達到iOS7流暢的滑動效果,,還保持了最低支持iOS6。

使用起來大概是這樣:

#import <UITableView+FDTemplateLayoutCell.h>
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return [tableView fd_heightForCellWithIdentifier:@"identifer" cacheByIndexPath:indexPath configuration:^(id cell) {
        // 配置 cell 的數(shù)據(jù)源,,和 "cellForRow" 干的事一致,,比如:
        cell.entity = self.feedEntities[indexPath.row];
    }];
}

寫完上面的代碼后,你就已經(jīng)使用到了:

  • 和每個UITableViewCell ReuseID一一對應(yīng)的template layout cell
這個cell只為了參加高度計算,,不會真的顯示到屏幕上,;它通過UITableView的-dequeueCellForReuseIdentifier: 方法lazy創(chuàng)建并保存,所以要求這個ReuseID必須已經(jīng)被注冊到了UITableView中,,也就是說,,要么是Storyboard中的原型cell,要么就是使用了UITableView的-registerClass:forCellReuseIdentifier:或-registerNib:forCellReuseIdentifier:其中之一的注冊方法,。
  • 根據(jù)autolayout約束自動計算高度
使用了系統(tǒng)在iOS 6就提供的API:-systemLayoutSizeFittingSize:
  • 根據(jù)index path的一套高度緩存機制
計算出的高度會自動進行緩存,,所以滑動時每個cell真正的高度計算只會發(fā)生一次,后面的高度詢問都會命中緩存,,減少了非??捎^的多余計算。
  • 自動的緩存失效機制
無須擔心你數(shù)據(jù)源的變化引起的緩存失效,,當調(diào)用如-reloadData,,-deleteRowsAtIndexPaths:withRowAnimation:等任何一個觸發(fā) UITableView 刷新機制的方法時,已有的高度緩存將以最小的代價執(zhí)行失效,。如刪除一個indexPath為[0:5]的cell時,,[0:0] ~ [0:4]的高度緩存不受影響,而[0:5]后面所有的緩存值都向前移動一個位置,。自動緩存失效機制對UITableView的9個公有API都進行了分別的處理,,以保證沒有一次多余的高度計算,。
  • 預(yù)緩存機制
預(yù)緩存機制將在UITableView沒有滑動的空閑時刻執(zhí)行,計算和緩存那些還沒有顯示到屏幕中的cell,,整個緩存過程完全沒有感知,,這使得完整列表的高度計算既沒有發(fā)生在加載時,又沒有發(fā)生在滑動時,,同時保證了加載速度和滑動流暢性,,下文會著重講下這塊的實現(xiàn)原理。

我們在設(shè)計這個工具的API時斟酌了非常長的時間,,既要保證功能的強大,,也要保證接口的精簡,一行調(diào)用背后隱藏著很多功能,。

這一套緩存機制能對滑動起多大影響呢,?除了肉眼能明顯的感知到外,我還做了個小測試,。一個有54個內(nèi)容和高度不同cell的table view,,從頭滑動到尾,再從尾滑動到頭,,iOS 8系統(tǒng)下,,iPhone 6,使用Time Profiler監(jiān)測算高函數(shù)所花費的時間:

  • 未使用緩存API,、未使用估算,,共花費877ms


  • 使用緩存API、開啟估算,,共花費77ms


測試數(shù)據(jù)的精度先不管,,從量級上就差了一個數(shù)量級,,說實話自己也沒想到差距有這么大,。

同時,工具也順手解決了-preferredMaxLayoutWidth的問題,,在計算高度前向contentView加了一條和table view寬度相同的寬度約束,,強行讓contentView內(nèi)部的控件知道了自己父view的寬度,再反算自己被外界約束的寬度,,破除“雞生蛋蛋生雞”的問題,,這里比較tricky,就不展開說了,。下面說說利用RunLoop預(yù)緩存的實現(xiàn),。

利用RunLoop空閑時間執(zhí)行預(yù)緩存任務(wù)

FDTemplateLayoutCell的高度預(yù)緩存是一個優(yōu)化功能,它要求頁面處于空閑狀態(tài)時才執(zhí)行計算,,當用戶正在滑動列表時顯然不應(yīng)該執(zhí)行計算任務(wù)影響滑動體驗,。

一般來說,,這個功能要耦合UITableView的滑動狀態(tài)才行,但這種實現(xiàn)十分不優(yōu)雅且可能破壞外部的delegate結(jié)構(gòu),,但好在我們還有RunLoop這個工具,,了解它的運行機制后,可以用很簡單的代碼實現(xiàn)上面的功能,。

空閑RunLoopMode

當用戶正在滑動UIScrollView時,,RunLoop將切換到UITrackingRunLoopMode接受滑動手勢和處理滑動事件(包括減速和彈簧效果),此時,,其他Mode(除NSRunLoopCommonModes這個組合Mode)下的事件將全部暫停執(zhí)行,,來保證滑動事件的優(yōu)先處理,這也是iOS滑動順暢的重要原因,。

當UI沒在滑動時,,默認的Mode是NSDefaultRunLoopMode(同CF中的kCFRunLoopDefaultMode),同時也是CF中定義的“空閑狀態(tài)Mode”,。當用戶啥也不點,,此時也沒有什么網(wǎng)絡(luò) IO時,就是在這個Mode下,。

用RunLoopObserver找準時機

注冊RunLoopObserver可以觀測當前RunLoop的運行狀態(tài),,并在狀態(tài)機切換時收到通知:

  1. RunLoop開始
  2. RunLoop即將處理Timer
  3. RunLoop即將處理Source
  4. RunLoop即將進入休眠狀態(tài)
  5. RunLoop即將從休眠狀態(tài)被事件喚醒
  6. RunLoop退出

因為“預(yù)緩存高度”的任務(wù)需要在最無感知的時刻進行,所以應(yīng)該同時滿足:

  1. RunLoop處于“空閑”狀態(tài)Mode,;
  2. 當這一次RunLoop迭代處理完成了所有事件,,馬上要休眠時。

使用CF的帶block版本的注冊函數(shù)可以讓代碼更簡潔:

CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFStringRef runLoopMode = kCFRunLoopDefaultMode;
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler
(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity _) {
    // TODO here
});
CFRunLoopAddObserver(runLoop, observer, runLoopMode);

在其中的TODO位置,,就可以開始任務(wù)的收集和分發(fā)了,,當然,不能忘記適時的移除這個observer,。

分解成多個RunLoop Source任務(wù)

假設(shè)列表有20個cell,,加載后展示了前5個,那么開啟估算后table view只計算了這5個的高度,,此時剩下15個就是“預(yù)緩存”的任務(wù),,而我們并不希望這15個計算任務(wù)在同一個RunLoop迭代中同步執(zhí)行,這樣會卡頓UI,,所以應(yīng)該把它們分別分解到15個RunLoop迭代中執(zhí)行,,這時就需要手動向RunLoop中添加Source任務(wù)(由應(yīng)用發(fā)起和處理的是Source 0任務(wù))

Foundation層沒對RunLoopSource提供直接構(gòu)建的API,但是提供了一個間接的,、既熟悉又陌生的API:

- (void)performSelector:(SEL)aSelector
               onThread:(NSThread *)thr 
             withObject:(id)arg 
          waitUntilDone:(BOOL)wait 
                  modes:(NSArray *)array;

這個方法將創(chuàng)建一個Source 0任務(wù),,分發(fā)到指定線程的RunLoop中,在給定的Mode下執(zhí)行,,若指定的RunLoop處于休眠狀態(tài),,則喚醒它處理事件,,簡單來說就是“睡你xx,起來嗨,!”

于是,,我們用一個可變數(shù)組裝載當前所有需要“預(yù)緩存”的index path,每個RunLoopObserver回調(diào)時都把第一個任務(wù)拿出來分發(fā):

NSMutableArray *mutableIndexPathsToBePrecached = self.fd_allIndexPathsToBePrecached.mutableCopy;
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler
(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity _) {
    if (mutableIndexPathsToBePrecached.count == 0) {
        CFRunLoopRemoveObserver(runLoop, observer, runLoopMode);
        return;
    }
    NSIndexPath *indexPath = mutableIndexPathsToBePrecached.firstObject;
    [mutableIndexPathsToBePrecached removeObject:indexPath];
    [self performSelector:@selector(fd_precacheIndexPathIfNeeded:)
                 onThread:[NSThread mainThread]
               withObject:indexPath
            waitUntilDone:NO
                    modes:@[NSDefaultRunLoopMode]];
});

這樣,,每個任務(wù)都被分配到下個“空閑”RunLoop迭代中執(zhí)行,,其間但凡有滑動事件開始,Mode切換成UITrackingRunLoopMode,,所有的“預(yù)緩存”任務(wù)的分發(fā)和執(zhí)行都會自動暫定,,最大程度保證滑動流暢。

本文原載于Sunny's Blog,,作者:孫源(@我就叫Sunny怎么了),,90后非主流iOS程序猿,現(xiàn)負責百度知道iOS團隊,。

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點,。請注意甄別內(nèi)容中的聯(lián)系方式,、誘導購買等信息,謹防詐騙,。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多