UITableViewCell 高度計算從混沌初始到天地交泰,uitableviewcell高度

來源:互聯網
上載者:User

UITableViewCell 高度計算從混沌初始到天地交泰,uitableviewcell高度
[原創]UITableViewCell 高度計算從混沌初始到天地交泰

 

本文主要基予iOS UITableViewCell 高度自適應計算問題展開陳述,廢話少說直入正題:

 

UITableView控制項可能是iOS中大家最常用的控制項了(滾動視圖、cell重用、卡頓最佳化),今天要討論的不是這些高大上的話題,今天的話題只是cell高度的計算。

* 傳統frame布局下UITableViewCell 高度計算

* AutoLayout下UITableViewCell高度計算(iOS6、7)

* UITableViewCell高度計算之iOS8抽風之旅

* UITableViewCell高度計算之大一統

*第三方庫UITableView-FDTemplateLayoutCell源碼拋析

以下demo都是在cell高度可變的基礎上進行的

一、傳統frame布局下UITableViewCell 高度計算

        1、史上最傳統的UITableViewCell使用方法(號稱又笨又老),相信大家都用過這種,純frame布局,cell定製,手動傳入資料通過手動計算每一行cell的高度,代碼都不好意思上了。

還是上下之前的demo吧!

01-UITableViewCell-frame

主要是在UITableViewCell(subCell)中使用一個靜態方法傳入資料並手動計算內容的高度

說到手動計算內容的高度,其實在cell裡面大多是計算一些UILabel具體的寬高,根據內容計算UILabel對應的寬高,看下具體的API:

@interface NSString(UIStringDrawing)// Single line, no wrapping. Truncation based on the NSLineBreakMode.- (CGSize)sizeWithFont:(UIFont*)fontNS_DEPRECATED_IOS(2_0,7_0,"Use -sizeWithAttributes:");- (CGSize)sizeWithFont:(UIFont*)font forWidth:(CGFloat)width lineBreakMode:(NSLineBreakMode)lineBreakModeNS_DEPRECATED_IOS(2_0,7_0,"Use -boundingRectWithSize:options:attributes:context:");// Wrapping to fit horizontal and vertical size. Text will be wrapped and truncated using the NSLineBreakMode. If the height is less than a line of text, it may return// a vertical size that is bigger than the one passed in.// If you size your text using the constrainedToSize: methods below, you should draw the text using the drawInRect: methods using the same line break mode for consistency- (CGSize)sizeWithFont:(UIFont*)font constrainedToSize:(CGSize)sizeNS_DEPRECATED_IOS(2_0,7_0,"Use -boundingRectWithSize:options:attributes:context:");// Uses NSLineBreakModeWordWrap- (CGSize)sizeWithFont:(UIFont*)font constrainedToSize:(CGSize)size lineBreakMode:(NSLineBreakMode)lineBreakModeNS_DEPRECATED_IOS(2_0,7_0,"Use -boundingRectWithSize:options:attributes:context:");// NSTextAlignment is not needed to determine size

這個地方Apple提供了一個NSString的分類,我們可以通過傳入對應的string 計算出label的自適應寬高,說到底就是使用sizeWithFont:系列重載函數

根據字串計算label的content大小。

代碼中使用:

(NSString一個傳統的方法sizeWithFont:)來計算label新的frame,然後更新布局,之後返回一個預計算的高度值

+ (CGFloat)calulateHeightWithtTitle:(NSString*)title{CGFloatheight =20;CGSizelabelSize = [titlesizeWithFont:[UIFont   systemFontOfSize:17] constrainedToSize:CGSizeMake(300,500)];height = height + labelSize.height;returnheight;}

 最終方法的調用在:

- (CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath{       return [HomeCell  calulateHeightWithtTitle:self.dataArray[indexPath.row]];}

 

來完成,並且return該float值作為cell的高度。

二、AutoLayout下UITableViewCell高度計算(iOS6、7)

1、下面介紹第二種方法,使用自動布局下的cell高度計算,總體來講,自動布局下 的cell高度計算歸功於UILabel的布局,自動布局下預設無需要再指定view的frame,設定完對應的約束,label會自動根據內容的多少來完成布局。廢話少說先上體驗版demo。

08-AutoLayoutCellHeight_ios7

上面描述到,傳統frame布局時間,主要是通過一些列手手動計算cell中label的寬高,然後在針對cell中的subView進行重新布局,最後得出一個整體的高度作為cell真實的高度,那麼在自動布局中又該如何?呢?首先自動布局一改了之前frame的概念,自動布局中不存在所謂的座標 寬高,只有對應的約束。針對UILabel來說,自動布局下label會根據內容的多少自適應的調整label的大小,顯示對應的內容。這一點先看下UILabel在iOS6以後發生的變化:

// Support for constraint-based layout (auto layout)// If nonzero, this is used when determining -intrinsicContentSize for multiline labels@property(nonatomic)CGFloat  preferredMaxLayoutWidthNS_AVAILABLE_IOS(6_0);

看到官方的注視,基本也大概有差不多的意思了,這東西實在autolayout下使用的,大概意思是給多行label設定一個布局時間優先使用的一個寬度。

在看下UIView的變化

@interfaceUIView (UIConstraintBasedLayoutFittingSize)/* The size fitting most closely to targetSize in which the receiver's subtree can be laid out while optimally satisfying the constraints. If you want the smallest possible size, pass UILayoutFittingCompressedSize; for the largest possible size, pass UILayoutFittingExpandedSize.Also see the comment for UILayoutPriorityFittingSizeLevel.*/- (CGSize)systemLayoutSizeFittingSize:(CGSize)targetSizeNS_AVAILABLE_IOS(6_0);// Equivalent to sending -systemLayoutSizeFittingSize:withHorizontalFittingPriority:verticalFittingPriority: with UILayoutPriorityFittingSizeLevel for both priorities.

 意思大概就是說 當前的這個這個尺寸關係能夠最佳的適應接收器的子樹在滿足自適應約束的同時,如果想要一個最下的尺寸就設定為:UILayoutFittingCompressedSize;反之設定:UILayoutFittingExpandedSize。

實戰應用:

自動布局下的自適應cell高度玩轉,本教程完全依賴storybord ,依舊在代碼UI領域的媛猿們,需要轉變一下思維了。

(1)、建立故事板、初始化好tableview、cell的輸出口等,準備cell的約束,

cell上只有一個label,label的約束如下,大體就是具體上下左右各加上一個約束,將來在label中放在對應的內容文字,自適應高度(不要忘了設定cell的identifier)。

 

(2)、部分實現處理代碼

ViewController中部分代理方法

- (NSInteger)numberOfSectionsInTableView:(UITableView*)tableView{return1;}- (NSInteger)tableView:(UITableView*)tableView numberOfRowsInSection:(NSInteger)section{       return [self.dataArraycount];}- (UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath{static NSString *cellIdentifier = @"HomeCell";HomeCell *cell = [tableView  dequeueReusableCellWithIdentifier:cellIdentifier];cell.content.text= [self.dataArray  objectAtIndex:indexPath.row];CGFloat  preMaxWaith =[UIScreen  mainScreen].bounds.size.width-108;[cell.contentset  PreferredMaxLayoutWidth:preMaxWaith];[cell.contentlayout  IfNeeded];returncell;}-(CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath{staticHomeCell*cell =nil;static dispatch_once_  tonceToken;//只會走一次dispatch_once(&onceToken, ^{cell = (HomeCell*)[tableView  dequeueReusableCellWithIdentifier:@"HomeCell"];});//calculateCGFloatheight = [cell  calulateHeightWithtTitle:[self.dataArray objectAtIndex:indexPath.row]desrip:[self.dataArray objectAtIndex:indexPath.row]];returnheight;}HomeCell.m -(CGFloat)calulateHeightWithtTitle:(NSString*)title desrip:(NSString*)descrip{//這裡非常重要CGFloat preMaxWaith =[UIScreen mainScreen].bounds.size.width-108;[self.contentset PreferredMaxLayoutWidth:preMaxWaith];//[self.titleLabel setText:title];//這也很重要[self.content  layoutIfNeeded];[self.content  setText:descrip];[self.contentView  layoutIfNeeded];CGSizesize = [self.contentView  systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];//加1是關鍵returnsize.height+1.0f;}

 自動布局版cell高度計算OK!!

三、UITableViewCell高度計算之iOS8抽風之旅

1、說到iOS8,在iOS8下如果要計算cell的高度,代碼越來越少,工作越來越輕鬆,殊不知表面看起來特別人性的iOS8背地裡面也有太多坑的勾當(具體原因見後面解釋)。

先上iOS的計算cell高度的體驗demo:

02-AutoLayout-iOS8-

iOS8下計算cell高度的工作比起之前的版本更加輕鬆

(1)、故事版拖好對應的VC、cell,接下來上約束,約束如下:

 

整體來說與2中的約束差不多,分別設定label距離四周的約束情況。(本篇文章要實現的本來就是相同的效果,在不同版本下的的實現方式以及優劣的對比與最佳化。)

設定好約束後

(2). iOS8的cell高度計算代碼

設定tableview的屬性

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

 

至此,iOS8cell高度自適應計算OK!! 就是這麼簡單...

四、UITableViewCell高度計算之大一統

 

在介紹本欄目之前先上一張表:

heightForRowAtIndexPath:cell高度計算次數統計:


heightForRowAtIndexPath:cell計算對比

由於iOS7之後,tableview 提供了estimatedHeightForRowAtIndexPathCount的API,這就對cell高度計算的方法調用次數產生了影響。

下面首先說下estimatedHeightForRowAtIndexPathCount :

// Use the estimatedHeight methods to quickly calcuate guessed values which will allow for fast load times of the table.// If these methods are implemented, the above -tableView:heightForXXX calls will be deferred until views are ready to be displayed, so more expensive logic can be placed there.- (CGFloat)tableView:(UITableView*)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath*)indexPathNS_AVAILABLE_IOS(7_0);

        apple的文檔裡面說大概意思是estimatedHeight可以快速的預估一個cell的高度值,從而讓table的載入速度更快。整體來說就是tableview在渲染的時間,他會首先根據內容計算每個cell的高度,從而計算出tableview的的一個contentsize(tableview繼承於scrollview),但是如果有一萬行資料,那麼這個計算的過程會非常的卡頓,從而影響table的load的速度,我們可以給cell(除了當前需要顯示在螢幕上的cell)設定一個預估的高度值,這樣就大大節省了計算高度的損耗與開銷。

    由可以看出,iOS7 的tableview對於cell的高度是有緩衝功能的,當划到底部,從底部再往頂部滑動時間,heightForRowAtIndexPath:cell的調用次數為0,這說話cell的高度已經換存在了記憶體。iOS8、iOS9坑爹的一面在於當關閉高度預估方法時間(estimatedHeightForRowAtIndexPathCount),heightForRowAtIndexPath:cell的調用次數非常多(我們一般會在這個地方計算根據內容手動計算cell的高度,或者更新cell內不各種view的約束),這個過程如果頻繁的調用是非常耗損效能的,更悲劇的事造成tableview的卡頓,這個是最容忍不了的。當開啟高度預估時間,高度預估之時返回了一個定值,此時heightForRowAtIndexPath:cell的調用次數大大減少,高度計算的工作也就大大減少,此時就是我們想要的效果。

     此外,這個地方有一個可能忽略的問題,當我們的工程從原來的iOS7遷移到8再到9的時間,如果這個地方不做進一步的最佳化,之前的代碼在新的系統下跑起來結果就不想而之了,為了能夠相容到所有系統下的cell高度計算,推薦一個新的工具

UITableView-FDTemplateLayoutCell

參考部落格:具體教程如

(1)、FDTemplateLayoutCell 使用教程

1. 下載FDTemplateLayoutCell第三方庫,匯入工程

 

2. 匯入標頭檔 

 

3. 使用FD實現heightForRowAtIndexPath方法,如下:

 

4、大功告成,使用fd一次性解決的iOS 6、7、8、9中的cell高度計算問題,FD採用內建的緩衝的機制,無需多次調用heightForRowAtIndexPath時間的cell高度計算開銷。 

1、FDTemplateLayoutCell之所以能夠做到相容到所有的系統版本下的tableview,主要在於它維護了一套自己的cell高度緩衝,同時有效利用了tableview的高度預估的功能。從新定義新的cell高度緩衝策略,這一點解決了只有iOS7下系統才會主動式快取cell高度的這一痛點,有了FDiOS8、9下也能使用緩衝高度

2、開啟UITableView高度預估功能,最佳化heightForRowAtIndexPath的調用累計次數

(tableView:estimatedHeightForRowAtIndexPath: NS_AVAILABLE_IOS(7_0);)

由上可以看出estimatedHeightForRowAtIndexPath是iOS7才有的,iOS6是沒有這個代理的,這個時間不僅要問,難道要iOS必須支援iOS7+以上才能使用,答案當然不是,系統的API早已做了最佳化,estimatedHeightForRowAtIndexPath在iOS6下面預設是可以被忽略的,不會因為版本的問題引起異常。在iOS6下高度計算的策略會跟iOS8、9下有點類似,使用FD自己提供的緩衝,也能達到有效減少計算cell高度帶來的開銷。

五、FDTemplateLayoutCell源碼拋析

     談到FD,首先熟悉下之前的一個知識點, iOS知識點整理-RunLoop。可能有些老生常談了,也有可能部分童鞋看到直接暈掉了,其實大多iOS裡面大多第三方庫的手段無外乎就是runtime(這個東西在java中叫reflact,java裡面有AOP , iOS 其實跟這個差不多)、CF這些黑魔法之類的東西來進行偷天換日、移花接木。

小結:iOS 中的runloop

        1、NSRunLoop提供了物件導向的API,但這些API不是安全執行緒的

        2、CFRunLoopRef提供了純C函數的API,所有這些API都是安全執行緒的

  NSRunLoop是cocoa提供的,這個東西可能大多人還是經常使用的,cell裡面更新非同步下載成功的圖片、啟用一個timer追加到當前的應用迴圈中、啟用一個常駐線程等;

  CFRunLoopRef可能就相對陌生些,CF開頭跟定就是CoreFoundation中定義的,可以暫時理解為每個線程都有一個對應的runloop, 在一個runloop中可以有多種Model(模式),每個Mode又包含若干個source/Timer/Observe .

程式執行的時間當前runloop 只能存在一種Model,如果發生情境切換需要退出當前Model,進入下一個Model

系統一共提供了五種model:

 NSDefaultRunLoopMode:    App預設Mode,當沒有接收到ScrollView滾動是,主線程通常使用這個Mode NSTrackingRunLoopMode:  到接收到ScroolView或其子類的時候,主線程就會切換到這個模式下運行。 UIInitializationRunLoopMode:當App啟動時使用的第一個Mode,當啟動完成後不再使用。 NSRunLoopCommonModes,是一個tag,本質上不是一個Mode,預設                    
NSDefaultRunLoopMode和NSTrackingRunLoopMode都綁定這個tag。(應用情境:有時候我們需要添加一個NSTimer在RunLoop,在這時需要制定一個Modes,現在的 需求是:我們既要在預設模式下要監聽,在滾動模式下也要監聽,但只能制定一個模式,這是可以制定這個CommonMode) GSEventReceiveRunLoopMode:接受系統內部的Mode,通常做不到。處理不同事件使用不同的Mode,可以最大限度的把效能的最大化處理不同分類的事件,提高效能。

   知道了這些,我們可以在此處做文章,我們發現UITableView(繼承UIScrollView)不滾動時間是NSDefaultRunLoopMode 模式,滾動時間是NSTrackingRunLoopMode模式,我們可以 通過註冊觀察者來實現讓tableview不滾動的時間再去計算所有的cell的高度,一旦當tableview開始滾動我們再去取得時間著時間緩衝池裡面已經計算 的差不多了,也就是說盡最大可能讓tableview不滾動的時間處理好所有的cell高度,緩衝下來,等到滑動tableview的時間優先從緩衝取,這個地方盡最大避免了邊滑動邊計算cell高度卡頓問題。

完成了這個知識點,接下來就是處理好緩衝邏輯的事情了。

1、首先對於FD來說,維護cell的高度需要將計算過的cell的高度放進一個二維數組裡面(section row)

    FD中存在一個可維護的NSMutableArray sections; 可以先理解為一個嵌套起來的數組是一個二位的數組,接下來的時間會把tableview 某個section下的row對應的行對應的高度存在這個位置,

2、tableView渲染的時間,統一還是會走 heightForRowAtIndexPath方法的,我們只需要在此處直接擷取到cache裡面的已經儲存的高度就行了,在此處避開cell的高度邏輯計算過程就到達了我們的目的。

FD組件已經作了很好的封裝,在heightForRowAtIndexPath中調用fd計算高度的方法,

- (CGFloat)fd_heightForCellWithIdentifier:(NSString*)identifier cacheByIndexPath:(NSIndexPath*)indexPath configuration:(void(^)(id))configuration{if(!identifier || !indexPath) {return0;}// Enable auto cache invalidation if you use this "cacheByIndexPath" API.if(!self.fd_autoCacheInvalidationEnabled) {self.fd_autoCacheInvalidationEnabled=YES;}// Enable precache if you use this "cacheByIndexPath" API.if(!self.fd_precacheEnabled) {self.fd_precacheEnabled=YES;// Manually trigger precache only for the first time.[selffd_precacheIfNeeded];}// Hit the cacheif([self.fd_cellHeightCachehasCachedHeightAtIndexPath:indexPath]) {[selffd_debugLog:[NSStringstringWithFormat:@"hit cache - [%@:%@] %@",@(indexPath.section),@(indexPath.row),@([self.fd_cellHeightCachecachedHeightAtIndexPath:indexPath])]];return[self.fd_cellHeightCachecachedHeightAtIndexPath:indexPath];}// Call basic height calculation method.CGFloatheight = [selffd_heightForCellWithIdentifier:identifierconfiguration:configuration];[selffd_debugLog:[NSStringstringWithFormat:@"cached - [%@:%@] %@",@(indexPath.section),@(indexPath.row),@(height)]];// Cache it[self.fd_cellHeightCachecacheHeight:heightbyIndexPath:indexPath];returnheight;}

 

 

這個步驟中,基本可以看出FD的使用原則,首先開啟一個[selffd_precacheIfNeeded]的操作(這個過程是做了一個預計算cell高度的操作,稍後詳解),接下來的過程就是從緩衝池中根據IndexPath(cell高度預儲存在一個類比的二維數組中)去讀取cell的高度,如果cache命中就直接返回cell高度,否則執行:

// Call basic height calculation method.CGFloatheight = [selffd_heightForCellWithIdentifier:identifierconfiguration:configuration];

 去手動計算一次cell的高度,計算獲得後存入緩衝池

// Cache it[self.fd_cellHeightCachecacheHeight:heightbyIndexPath:indexPath];

 最後返回高度。

3、介紹FD的緩衝池

FD在這個地方利用了runloop的黑魔法,通過註冊一個觀察者,當tableview停止滑動的他會主動去計算當前資料來源中的剩餘的cell的高度,計算完以後儲存在緩衝池中,這個調用就是(2)中的

// Enable precache if you use this "cacheByIndexPath" API.if(!self.fd_precacheEnabled) {self.fd_precacheEnabled=YES;// Manually trigger precache only for the first time.[selffd_precacheIfNeeded];}

 在這個開啟調用中,通過一些列手段將tableview不滾動時間去計算cell的高度(具體原理此處省略),計算後存入緩衝池sections,sections是一個可變數組,筆者顯示把這個理解成一個記憶體儲存元素是可變數組的數組(類比一個二維數組),FD先是給自己增加了一個屬性sections作為緩衝池,通過objc_setAssociatedObject給分類增加屬性的此處就不介紹了,

[selfbuildHeightCachesAtIndexPathsIfNeeded:@[indexPath]];self.sections[indexPath.section][indexPath.row] =@(height);// Build every section array or row array which is smaller than given index path.[indexPaths enumerateObjectsUsingBlock:^(NSIndexPath*indexPath,NSUIntegeridx,BOOL*stop) {NSAssert(indexPath.section>=0,@"Expect a positive section rather than '%@'.",@(indexPath.section));for(NSIntegersection =0; section <= indexPath.section; ++section) {if(section >=self.sections.count) {self.sections[section] =@[].mutableCopy;}}NSMutableArray*rows =self.sections[indexPath.section];for(NSIntegerrow =0; row <= indexPath.row; ++row) {if(row >= rows.count) {rows[row] =@(_FDTemplateLayoutCellHeightCacheAbsentValue);}}}];

 此處主要是構造一個緩衝池,通過在sections中儲存一個NSMutableArray,類比一個二維的數組

通過indexPath的section 和 row作為下標,構造完成直接將高度存進去:

self.sections[indexPath.section][indexPath.row] =@(height);

至此緩衝池結束

4、至此FD的核心手段大題已經講完,接下來就是考慮到tableview的增刪改插的時間的處理問題,這一系列的動作都會對緩衝池的更新有一定的影響,FD已經做了最大的限度的最佳化,依舊runtime, swizzling的魔法就不多解釋了。

dispatch_once(&onceToken, ^{SELselectors[] = {@selector(reloadData),@selector(insertSections:withRowAnimation:),@selector(deleteSections:withRowAnimation:),@selector(reloadSections:withRowAnimation:),@selector(moveSection:toSection:),@selector(insertRowsAtIndexPaths:withRowAnimation:),@selector(deleteRowsAtIndexPaths:withRowAnimation:),@selector(reloadRowsAtIndexPaths:withRowAnimation:),@selector(moveRowAtIndexPath:toIndexPath:)};for(NSUIntegerindex =0; indexSELoriginalSelector = selectors[index];SELswizzledSelector =NSSelectorFromString([@"fd_"stringByAppendingString:NSStringFromSelector(originalSelector)]);MethodoriginalMethod =class_getInstanceMethod(self, originalSelector);MethodswizzledMethod =class_getInstanceMethod(self, swizzledSelector);method_exchangeImplementations(originalMethod, swizzledMethod);}});

小結:

FD是一個封裝的很完美的庫,其實從一開始使用這個庫就喜歡上了,作者是百度的sunnyxy,另一方面iOS中runtime仍舊是一個很強大的東西,大多的第三方庫無非都是基本objc runtime做的一些便捷最佳化,但是一個優秀的第三方庫需要作者不斷的完善和大家的共同努力。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.