iOS 開發中 UITableView 的性能優化
iOS開發中行高靈活可變的UITableView的性能優化
一、UITableView的構建原理
在新聞類,電商類等應用中,應用著大量的圖文混排視圖,在表視圖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方法進行子視圖布局時會再次把所有行高數據進行拉取。
TableViewCell配置部分:
④當使用cellID進行與TableView綁定的cell獲取時會拉取本行cell的高度數據。
⑤當cell進行layoutSubViews方法進行布局時會再次拉取本行cell的高度數據。
上面列舉的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位數,則這個方法的執行次數將會十分恐怖:imp:。
至于為何UITableView在進行配置時也需要拉取所有的行高數據,我猜想其為了進行視圖的一些初始化操作,例如表視圖右側滾動條的寬度和所占比例等。并且,每次拉取高度都從代理方法拉取,而不是存入內部的一個變量屬性中,避免了因為數據源更改時機巧合而產生的界面與預期不一致的風險。
二、對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;
}</code></pre>

通過上面示例可以看到,十分簡單的代碼完美的解決了圖文混排cell高度的自適應。Autolayout真的是一種十分強大的技術:smile:。
關于細節方面,還有一個問題需要注意,預估的行高會影響到TableView右側滾動條的展現,如果每個cell行高跳躍跨度十分大,滾動條寬度的配置會失準,隨著用戶滑動表視圖,右側滾動條可能會出現長短跳躍的情況,如果開發者需要精準這個滾動條的配置,可以在如下代理方法中返回具體cell的估計行高。
-(CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath{
//這里根據不同分區 或者不同行 設置估計的行高
return 44;
}
關于estimatedHeightForRowAtIndexPath方法其實還有一種應用場景,前面介紹的優化方式都是以Autolyout為前提,對于沒有使用自動布局,cell的高度需要手動計算的場景中,如果實現了這個方法,并且實現了heightForRowAtIndexPath方法,heightForRowAtIndexPath方法會以懶加載的方式執行,只有在cell將要展現在屏幕上時heightForRowAtIndexPath方法才會被執行,這也可以有效減小由于高度計算帶來的性能負擔。
三、關于高度不定的UITableView分區頭尾視圖
一般情況下,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;
}</code></pre>
效果如下圖:

分區為視圖的設置方式與頭視圖一樣。
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 @"頭視圖頭視圖頭視圖頭視圖頭視圖頭視圖頭視圖頭視";
}
}</code></pre>
小提示:UITableViewCell在創建出來時,其寬度并不一定和UITableView寬度一致,如果開發者需要通過獲取cell的寬度來處理邏輯,要在cell的layoutSubViews里面進行,此時cell的寬度才正確。
來自:http://my.oschina.net/u/2340880/blog/738702