在新聞類,電商類等應用中,應用着大量的圖文混排視圖,在表視圖UITableView中,開發者一般須要在以下代理方法中計算出當前cell填充內容後的高度,以後將其返回:數組
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{ //先根據數據源中數據計算高度 CGFloat height = 0; return height; }
然而,若是在如上方法中進行打印調試能夠發現,heightForRowAtIndexPath方法會重複執行好屢次,首先,而且heightForRowAtIndexPath方法的執行機制在不一樣版本的iOS系統還會有很大不一樣。以iOS9爲例,一行cell要展現在屏幕上,至少要執行5遍TableView的heightForRowAtIndexPath方法:性能優化
TableView配置部分:佈局
① 當TableView視圖即將展示在屏幕上時,會把全部行的行高數據進行拉取。性能
②當TableView在執行setLayoutMargins方法進行自身佈局時會把全部行高數據進行拉取。優化
③TableView在執行layoutSubViews方法進行子視圖佈局時會再次把全部行高數據進行拉取。spa
TableViewCell配置部分:代理
④當使用cellID進行與TableView綁定的cell獲取時會拉取本行cell的高度數據。調試
⑤當cell進行layoutSubViews方法進行佈局時會再次拉取本行cell的高度數據。code
上面列舉的5中拉取cell高度的場景中,TableView配置部分只會在TableView第一次展示在屏幕上時出現,可是其拉取的是全部行的行高數據,若是表視圖有100行或者更多,這將是一個十分耗費性能的過程。TableViewCell配置部分,只有當cell將要出如今屏幕上時纔會出現,而且只拉取當前行的行高,這兩種場景會在用戶滑動TableView時不斷被執行,而且根據UITableView的佈局cell原理,系統會默認準備當前一屏高度所能容納cell個數加1個cell。開發
當執行TableView的reloadData方法進行界面刷新時,系統先會把全部行的行高數據拉取一遍,以後和UITableViewCell配置部分的場景一直,會拉取即將出如今屏幕上的cell的行高數據。
用示意圖形象的表示上述邏輯以下:
經過上面分析,以10行數據的表格視圖爲例,若一屏幕能夠呈現7行數據(TableView須要準備8行),則在第一次展現TableView視圖時,會執行44次heightForRwoAtIndexPath方法,每次刷新TableView須要執行24次heightForRwoAtIndexPath方法,若是TableView的行數增長到3位數,則這個方法的執行次數將會十分恐怖👿。
至於爲什麼UITableView在進行配置時也須要拉取全部的行高數據,我猜測其爲了進行視圖的一些初始化操做,例如表視圖右側滾動條的寬度和所佔比例等。而且,每次拉取高度都從代理方法拉取,而不是存入內部的一個變量屬性中,避免了由於數據源更改時機巧合而產生的界面與預期不一致的風險。
經過前面的分析,能夠理解若是將複雜的計算代碼寫在heightForRowAtIndexPath方法中,代價將是很是慘重的。滑動不流暢,屏幕卡頓不少性能問題都是因爲這個緣由。對於行高固定的表格視圖,開發者能夠直接設置TableView的固定行高,以下:
_tableView.rowHeight = 200;
若是行高是不固定了,則應該想辦法讓heightForRowAtIndexPath方法完成最少的工做,其實最少的工做莫過於拿過一個高度,直接返回,所以開發者一般會將對應行的行高計算一次後,把值進行保存,以後在執行heightForRowAtIndexPath方法拉取行高時,直接返回已經計算過的行高數據,具體如何操做比較靈活,能夠對應一個數組屬性,將計算後的行高放入數組中,每次取行高時,檢查數組中是否已經有計算過的行高數據,若是有直接返回。我我的更傾向將行高數據封裝進cell的數據模型Model中。
經過優化,能夠有效的減小重複的高度計算,這也是我原先處理此類問題的主要方式。然而,只是提升了代碼的性能,對開發者來講,工做量和複雜度有增而無減。在開發中一般會遇到一些十分複雜的界面,而這些界面中cell的高度都是須要經過請求到的數據動態改變的,每一個cell都要寫複雜的尺寸計算代碼十分使人心煩。在iOS7以後,系統提供了一種自動計算cell高度的方法,這不管在性能仍是工做量上,都徹底解放了開發者。
在iOS7系統以後,UITableView類中增長了一個estimatedRowHeight屬性,顧名思義,這個屬性是設置UITableViewCell中的大約行高值。這個值設置以後,開發者無需設置rowHeight屬性,也不須要實現heightForRowAtIndexPath方法,系統會自動根據UITableViewCell中contentView的約束來計算本身的行高。estimatedRowHeight屬性用於TableView進行初始化,其會影響到表格視圖右側滾動條的寬度。cell展示出來時真正的行高並不受這個屬性值的影響。
那麼如今問題來了,如何才能讓cell正確計算本身的高度,這就要使用到Autolayout了,不管是經過xib文件建立的cell仍是代碼建立的cell,若想讓cell自動正確的計算出自身的高度,必須添加足夠壓力的約束。所謂足夠壓力,是指UITableViewCell的contentView的上、下、左、右必須被內部控件的約束所撐滿,須要注意,cell上的視圖必須添加在contentView上,不然計算會出現問題。
例以下圖所示,左側的圖標進行了與父視圖的左側距離約束,標題Label進行了與父視圖的上側距離約束和右側距離約束,內容Label進行了與標題Label的上側約束和與父視圖的下冊約束,而且對寬度進行了約束。此時,UITableViewCell的contentView四周都被子視圖進行了約束,能夠想象,內容Label的文本長度是不定的,當文本長度是的內容Label進行換行,內容Label的高度改變的時候,contentView下冊會受到內容Label施加的壓力,這時cell也會根據約束自動擴充本身的高度。
示例代碼以下:
- (void)viewDidLoad { [super viewDidLoad]; self.title = @"表視圖"; _tableView = [[UITableView alloc]initWithFrame:self.view.frame style:UITableViewStylePlain]; [_tableView registerNib:[UINib nibWithNibName:@"TableViewCell" bundle:nil] forCellReuseIdentifier:@"cellid"]; _tableView.delegate = self; _tableView.dataSource = self; //設置一個模糊的行高用於配置TableView右側滾動條 _tableView.estimatedRowHeight = 60; [self.view addSubview:_tableView]; titleArray = @[@"標題1",@"標題2",@"標題3",@"標題4",@"標題5",@"標題6",@"標題7",@"標題8",@"標題9",@"標題10"]; detailArray = @[@"內容內容內容內容內容內容內容內容內容", @"內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容", @"內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容", @"內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容", @"內容內容容內容內內容內容內容內容內容內容內容", @"容內容內內容內容", @"內容內容內容內容容內容內容內容", @"內容內容內容內容內容內容內容內容內容內容內容內容內容內容內容", @"內容內容內容內容內容內容容內容內內容內容內容", @"內容內容內容內容內容內容內容內容內容"]; } -(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{ return 10; } -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ TableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:@"cellid" forIndexPath:indexPath]; cell.title.text = titleArray[indexPath.row]; cell.detail.text = detailArray[indexPath.row]; return cell; }
經過上面示例能夠看到,十分簡單的代碼完美的解決了圖文混排cell高度的自適應。Autolayout真的是一種十分強大的技術😄。
關於細節方面,還有一個問題須要注意,預估的行高會影響到TableView右側滾動條的展示,若是每一個cell行高跳躍跨度十分大,滾動條寬度的配置會失準,隨着用戶滑動表視圖,右側滾動條可能會出現長短跳躍的狀況,若是開發者須要精準這個滾動條的配置,能夠在以下代理方法中返回具體cell的估計行高。
-(CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath{ //這裏根據不一樣分區 或者不一樣行 設置估計的行高 return 44; }
關於estimatedHeightForRowAtIndexPath方法其實還有一種應用場景,前面介紹的優化方式都是以Autolyout爲前提,對於沒有使用自動佈局,cell的高度須要手動計算的場景中,若是實現了這個方法,而且實現了heightForRowAtIndexPath方法,heightForRowAtIndexPath方法會以懶加載的方式執行,只有在cell將要展示在屏幕上時heightForRowAtIndexPath方法纔會被執行,這也能夠有效減少因爲高度計算帶來的性能負擔。
通常狀況下,TableView的分區頭尾視圖高度都是固定的,所以通常不須要考慮計算分區頭尾視圖高度產生的性能問題,類好比cell的佈局原理,其實分區頭尾視圖也能夠經過Autolayout實現自適應高度,示例代碼以下:
//返回一個估計的分區頭視圖高度 -(CGFloat)tableView:(UITableView *)tableView estimatedHeightForHeaderInSection:(NSInteger)section{ return 10; } //使用自動佈局給頭視圖添加足夠的佈局壓力 -(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section{ UIView * view = [[UIView alloc]init]; UILabel * label = [[UILabel alloc]init]; label.numberOfLines = 0; if (section==0) { label.text = @"頭視圖頭視圖頭視圖"; }else{ label.text = @"頭視圖頭視圖頭視圖頭視圖頭視圖頭視圖頭視圖頭視圖頭視圖頭視圖頭視圖頭視圖頭視圖頭視圖頭視圖頭視圖頭視圖頭視圖頭視圖頭視圖頭視圖頭視圖頭視圖頭視圖頭視圖頭視圖頭視圖頭視圖頭視圖頭視圖"; } [view addSubview:label]; [label mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(@10); make.right.equalTo(@-10); make.top.equalTo(@10); make.bottom.equalTo(@-10); }]; return view; }
效果以下圖:
分區爲視圖的設置方式與頭視圖同樣。
UITableView類中還有一個十分有趣的常量:
UIKIT_EXTERN const CGFloat UITableViewAutomaticDimension;
UITableViewAutomaticDimension是一個CGFloat類型的常量,其須要和用來處理返回頭尾視圖標題的方法結合使用,用它來做爲TableView分區頭尾視圖的高度返回,系統會自動根據標題是否存在來進行自適應,舉個例子,若是返回的標題爲nil,則頭視圖會被自動隱藏,示例代碼以下:
-(CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section{ //視圖爲nil則會自動返回0 return UITableViewAutomaticDimension; } -(NSString*)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section{ if (section==0) { return nil; }else{ return @"頭視圖頭視圖頭視圖頭視圖頭視圖頭視圖頭視圖頭視"; } }
小提示:UITableViewCell在建立出來時,其寬度並不必定和UITableView寬度一致,若是開發者須要經過獲取cell的寬度來處理邏輯,要在cell的layoutSubViews裏面進行,此時cell的寬度才正確。
專一技術,熱愛生活,交流技術,也作朋友。
——琿少 QQ羣:203317592