前言:
給大家推薦一個開源的擴展贸伐,UITableView+FDTemplateLayoutCell,讓高度計算這個事情變的前所未有的簡單踩衩。以下對?UITableViewCell?利用?AutoLayout?自動高度計算和?UITableView?滑動優(yōu)化的一個總結衙传。
這篇總結你可以讀到:
UITableView高度計算和估算的機制
不同iOS系統(tǒng)在高度計算上的差異
iOS8 self-sizing cell
UITableView+FDTemplateLayoutCell如何用一句話解決高度問題
UITableView+FDTemplateLayoutCell中對RunLoop的使用技巧
UITableViewCell高度計算
# rowHeight
UITableView是我們再熟悉不過的視圖了指蚁,它的?delegate?和?data source?回調不知寫了多少次,也不免遇到 UITableViewCell 高度計算的事颈娜。UITableView 詢問 cell 高度有兩種方式剑逃。
一種是針對所有 Cell 具有固定高度的情況,通過:
上面的代碼指定了一個所有 cell 都是 88 高度的 UITableView官辽,對于定高需求的表格蛹磺,強烈建議使用這種(而非下面的)方式保證不必要的高度計算和調用。rowHeight屬性的默認值是 44同仆,所以一個空的 UITableView 顯示成那個樣子萤捆。
另一種方式就是實現(xiàn) UITableViewDelegate 中的:
需要注意的是誉尖,實現(xiàn)了這個方法后蜀变,rowHeight?的設置將無效议薪。所以凰荚,這個方法適用于具有多種 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 在一開始并不知道自己會被填充多少內容,于是詢問 data source 個數(shù)和創(chuàng)建 cell挽封,同時詢問 delegate 這些 cell 應該顯示的高度已球,這就造成它在加載的時候浪費了多余的計算在屏幕外邊的 cell 上。和上面的?rowHeight?很類似辅愿,設置這個估算高度有兩種方法:?
self.tableView.estimatedRowHeight = 88; ? ?// or ? ?
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath { ? ? ? ?// return xxx ? ?}
有所不同的是智亮,即使面對種類不同的 cell,我們依然可以使用簡單的?estimatedRowHeight?屬性賦值点待,只要整體估算值接近就可以阔蛉,比如大概有一半 cell 高度是 44, 一半 cell 高度是 88癞埠, 那就可以估算一個 66状原,基本符合預期。
說完了估算高度的基本使用苗踪,可以開始吐槽了:?
設置估算高度后颠区,contentSize.height 根據(jù)“cell估算值 x cell個數(shù)”計算,這就導致滾動條的大小處于不穩(wěn)定的狀態(tài)通铲,contentSize 會隨著滾動從估算高度慢慢替換成真實高度毕莱,肉眼可見滾動條突然變化甚至“跳躍”。
若是有設計不好的下拉刷新或上拉加載控件颅夺,或是 KVO 了 contentSize 或 contentOffset 屬性央串,有可能使表格滑動時跳動。
估算高度設計初衷是好的碗啄,讓加載速度更快质和,那憑啥要去侵害滑動的流暢性呢,用戶可能對進入頁面時多零點幾秒加載時間感覺不大稚字,但是滑動時實時計算高度帶來的卡頓是明顯能體驗到的饲宿,個人覺得還不如一開始都算好了呢(iOS8更過分,即使都算好了也會邊劃邊計算)
iOS8 self-sizing cell
具有動態(tài)高度內容的 cell 一直是個頭疼的問題胆描,比如聊天氣泡的 cell瘫想, frame 布局時代通常是用數(shù)據(jù)內容反算高度:
? ? CGFloat height = textHeightWithFont() + imageHeight + topMargin + bottomMargin + ...;
供 UITableViewDelegate 調用時很可能是個 cell 的類方法:
?@interface BubbleCell : UITableViewCell
+ (CGFloat)heightWithEntity:(id)entity;
@end
各種魔法 margin 加上耦合了屏幕寬度。
AutoLayout 時代好了不少昌讲,提供了-systemLayoutSizeFittingSize:的 API国夜,在 contentView 中設置約束后,就能計算出準確的值短绸;缺點是計算速度肯定沒有手算快车吹,而且這是個實例方法筹裕,需要維護專門為計算高度而生的?template layout cell,它還要求使用者對約束設置的比較熟練窄驹,要保證 contentView 內部上下左右所有方向都有約束支撐朝卒,設置不合理的話計算的高度就成了0。?
這里還不得不提到一個 UILabel 的蛋疼問題乐埠,當 UILabel 行數(shù)大于0時抗斤,需要指定?preferredMaxLayoutWidth?后它才知道自己什么時候該折行。這是個“雞生蛋蛋生雞”的問題丈咐,因為 UILabel 需要知道 superview 的寬度才能折行瑞眼,而 superview 的寬度還依仗著子 view 寬度的累加才能確定。這個問題好像到 iOS8 才能夠自動解決(不過我們找到了解決方案)
回到正題棵逊,iOS8 WWDC 中推出了?self-sizing cell?的概念负拟,旨在讓 cell 自己負責自己的高度計算,使用 frame layout 和 auto layout 都可以享受到:
這個特性首先要求是 iOS8歹河,要是最低支持的系統(tǒng)版本小于8的話,還得針對老版本單寫套老式的算高(囧)花吟,不過用的 API 到不是新面孔:
self.tableView.estimatedRowHeight = 213;? ? ? ? ? ?
self.tableView.rowHeight = UITableViewAutomaticDimension;
這里又不得不吐槽了秸歧,自動計算 rowHeight 跟 estimatedRowHeight 到底是有什么仇,如果不加上估算高度的設置衅澈,自動算高就失效了- -
PS:iOS8 系統(tǒng)中 rowHeight 的默認值已經(jīng)設置成了 UITableViewAutomaticDimension键菱,所以第二行代碼可以省略。?
問題:?
這個自動算高在 push 到下一個頁面或者轉屏時會出現(xiàn)高度特別詭異的情況今布,不過現(xiàn)在的版本修復了经备。?
求一個能讓最低支持 iOS8 的公司- -.
iOS8抽風的算高機制
相同的代碼在 iOS7 和 iOS8 上滑動順暢程度完全不同,iOS8 莫名奇妙的卡部默。很大一部分原因是 iOS8 上的算高機制大不相同侵蒙,這是我做的小測試:
研究后發(fā)現(xiàn)這么多次額外計算有下面的原因:
不開啟高度估算時,UITableView 上來就要對所有 cell 調用算高來確定 contentSize
dequeueReusableCellWithIdentifier:forIndexPath:?相比不帶 “forIndexPath” 的版本會多調用一次高度計算
iOS7 計算高度后有”緩存“機制傅蹂,不會重復計算纷闺;而 iOS8 不論何時都會重新計算 cell 高度
iOS8 把高度計算搞成這個樣子,從 WWDC 也倒是能找到點解釋份蝴,cell 被認為隨時都可能改變高度(如從設置中調整動態(tài)字體大欣绻Α),所以每次滑動出來后都要重新計算高度婚夫。?
說了這么多浸卦,究竟有沒有既能省去算高煩惱,又能保證順暢的滑動案糙,還能支持 iOS6+ 的一站式解決方案呢限嫌?
UITableView+FDTemplateLayoutCell
使用?UITableView+FDTemplateLayoutCell?無疑是解決算高問題的最佳實踐之一靴庆,既有 iOS8 self-sizing 功能簡單的 API,又可以達到 iOS7 流暢的滑動效果萤皂,還保持了最低支持 iOS6撒穷。
使用起來大概是這樣:
#import<UITableView+FDTemplateLayoutCell>
?- (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 一一對應的 template layout cell
這個 cell 只為了參加高度計算,不會真的顯示到屏幕上入录;它通過 UITableView 的?-dequeueCellForReuseIdentifier:?方法 lazy 創(chuàng)建并保存蛤奥,所以要求這個 ReuseID 必須已經(jīng)被注冊到了 UITableView 中,也就是說僚稿,要么是 Storyboard 中的原型 cell凡桥,要么就是使用了 UITableView 的?-registerClass:forCellReuseIdentifier:?或?-registerNib:forCellReuseIdentifier:其中之一的注冊方法。?
根據(jù) autolayout 約束自動計算高度
使用了系統(tǒng)在 iOS6 就提供的 API:-systemLayoutSizeFittingSize:
根據(jù) index path 的一套高度緩存機制
計算出的高度會自動進行緩存蚀同,所以滑動時每個 cell 真正的高度計算只會發(fā)生一次缅刽,后面的高度詢問都會命中緩存,減少了非炒缆纾可觀的多余計算衰猛。
自動的緩存失效機制
無須擔心你數(shù)據(jù)源的變化引起的緩存失效,當調用如-reloadData刹孔,-deleteRowsAtIndexPaths:withRowAnimation:等任何一個觸發(fā) UITableView 刷新機制的方法時啡省,已有的高度緩存將以最小的代價執(zhí)行失效。如刪除一個 indexPath 為 [0:5] 的 cell 時髓霞,[0:0] ~ [0:4] 的高度緩存不受影響卦睹,而 [0:5] 后面所有的緩存值都向前移動一個位置。自動緩存失效機制對 UITableView 的 9 個公有 API 都進行了分別的處理方库,以保證沒有一次多余的高度計算结序。?
預緩存機制
預緩存機制將在 UITableView 沒有滑動的空閑時刻執(zhí)行,計算和緩存那些還沒有顯示到屏幕中的 cell纵潦,整個緩存過程完全沒有感知笼痹,這使得完整列表的高度計算既沒有發(fā)生在加載時,又沒有發(fā)生在滑動時酪穿,同時保證了加載速度和滑動流暢性凳干,下文會著重講下這塊的實現(xiàn)原理。
我們在設計這個工具的 API 時斟酌了非常長的時間被济,既要保證功能的強大救赐,也要保證接口的精簡,一行調用背后隱藏著很多功能。
這一套緩存機制能對滑動起多大影響呢经磅?除了肉眼能明顯的感知到外泌绣,我還做了個小測試:
一個有 54 個內容和高度不同 cell 的 table view,從頭滑動到尾预厌,再從尾滑動到頭阿迈,iOS8 系統(tǒng)下,iPhone6轧叽,使用?Time Profiler?監(jiān)測算高函數(shù)所花費的時間:?
未使用緩存API苗沧、未使用估算,共花費 877 ms:
使用緩存API炭晒、開啟估算待逞,共花費 77 ms:
測試數(shù)據(jù)的精度先不管,從量級上就差了一個數(shù)量級网严,說實話自己也沒想到差距有這么大- -?
同時识樱,工具也順手解決了-preferredMaxLayoutWidth的問題,在計算高度前向 contentView 加了一條和 table view 寬度相同的寬度約束震束,強行讓 contentView 內部的控件知道了自己父 view 的寬度怜庸,再反算自己被外界約束的寬度,破除“雞生蛋蛋生雞”的問題垢村,這里比較 tricky割疾,就不展開說了。下面說說利用 RunLoop 預緩存的實現(xiàn)肝断。
利用RunLoop空閑時間執(zhí)行預緩存任務
FDTemplateLayoutCell 的高度預緩存是一個優(yōu)化功能,它要求頁面處于空閑狀態(tài)時才執(zhí)行計算驰凛,當用戶正在滑動列表時顯然不應該執(zhí)行計算任務影響滑動體驗胸懈。
一般來說,這個功能要耦合 UITableView 的滑動狀態(tài)才行恰响,但這種實現(xiàn)十分不優(yōu)雅且可能破壞外部的 delegate 結構趣钱,但好在我們還有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)絡 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退出
因為“預緩存高度”的任務需要在最無感知的時刻進行,所以應該同時滿足:
1.RunLoop 處于“空閑”狀態(tài) Mode
2.當這一次 RunLoop 迭代處理完成了所有事件挣棕,馬上要休眠時
使用 CF 的帶 block 版本的注冊函數(shù)可以讓代碼更簡潔:
在其中的 TODO 位置译隘,就可以開始任務的收集和分發(fā)了,當然穴张,不能忘記適時的移除這個 observer
#?分解成多個RunLoop Source任務
假設列表有 20 個 cell细燎,加載后展示了前 5 個,那么開啟估算后 table view 只計算了這 5 個的高度皂甘,此時剩下 15 個就是“預緩存”的任務玻驻,而我們并不希望這 15 個計算任務在同一個 RunLoop 迭代中同步執(zhí)行,這樣會卡頓 UI偿枕,所以應該把它們分別分解到 15 個 RunLoop 迭代中執(zhí)行璧瞬,這時就需要手動向 RunLoop 中添加 Source 任務(由應用發(fā)起和處理的是 Source 0 任務)
Foundation 層沒對 RunLoopSource 提供直接構建的 API,但是提供了一個間接的渐夸、既熟悉又陌生的 API:
?- (void)performSelector:(SEL)aSelector ?
? ? ? ? ? ? ? ? ? ? ? ? ?onThread:(NSThread *)thr ?
? ? ? ? ? ? ? ? ? ? ? ? withObject:(id)arg ? ? ? ?
? ? ? ? ? ? ? ? ? ? waitUntilDone:(BOOL)wait? ?
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? modes:(NSArray *)array;
這個方法將創(chuàng)建一個 Source 0 任務嗤锉,分發(fā)到指定線程的 RunLoop 中,在給定的 Mode 下執(zhí)行墓塌,若指定的 RunLoop 處于休眠狀態(tài)瘟忱,則喚醒它處理事件,簡單來說就是“睡你xx苫幢,起來嗨访诱!”
于是,我們用一個可變數(shù)組裝載當前所有需要“預緩存”的 index path韩肝,每個 RunLoopObserver 回調時都把第一個任務拿出來分發(fā):
這樣触菜,每個任務都被分配到下個“空閑” RunLoop 迭代中執(zhí)行,其間但凡有滑動事件開始哀峻,Mode 切換成 UITrackingRunLoopMode涡相,所有的“預緩存”任務的分發(fā)和執(zhí)行都會自動暫定,最大程度保證滑動流暢剩蟀。?
PS: 預緩存功能因為下拉刷新的沖突和不明顯的收益已經(jīng)廢棄
開始使用UITableView+FDTemplateLayoutCell
如果你覺得這個工具能幫得到你催蝗,整合到工程也十分簡單。
使用 cocoapods:
pod search UITableView+FDTemplateLayoutCell
歡迎使用和支持這個工具:
github 地址:?https://github.com/forkingdog/UITableView-FDTemplateLayoutCell
原文出自:http://blog.sunnyxx.com/2015/05/17/cell-height-calculation/