從自適應單元格高度提及-淺談如何提升UITableView的加載效率

你們基本上都作過這樣的需求:在UITableView上展現文本,且文本內容長短不一,每一行單元格都要動態計算高度,使得單元格能夠恰好容納下須要展現的文字。爲了方便講解,咱們把文本框設定成一個距離cell上下左右均有20px間距的UILabel,須要單元格動態調整高度,使得文本框恰好能夠展現出全部的文本內容。git

實現方案

需求自己並非很是複雜,實現這個需求基本上能夠採用兩種方法:github

一、代碼動態計算高度數組

二、利用iOS8中UITableView的estimatedRowHeight新特性經過約束計算高度緩存

咱們先來看一下兩種方案的實現方式:微信

代碼動態計算高度

在UITableViewCell的自定義類中增長一個計算cell高度的類方法,具體代碼以下:app

+ (CGFloat)calculateTitleWidth:(NSString *)title{
    
    CGFloat stringWidth = 0;
    CGSize size = CGSizeMake(kRBScreenWidth - 20.0f*2, MAXFLOAT);
    
    if (title.length > 0) {
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000
        stringWidth = [title
                      boundingRectWithSize:size
                      options:NSStringDrawingUsesLineFragmentOrigin
                      attributes:@{NSFontAttributeName:kRBTextFont}
                      context:nil].size.height;
#else
        //iOS7.0如下方法
        stringWidth = [title sizeWithFont:kRBTextFont
                            constrainedToSize:size
                                lineBreakMode:NSLineBreakByCharWrapping].height;
#endif
    }
    return stringWidth;
}
複製代碼

當咱們經過- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath方法獲得對應的cell以後,調用cell的- (void)buildData:(NSString *)title方法,填充文本,設置文本框高度:佈局

- (void)buildData:(NSString *)title{
    
    self.titleLabel.text = title;
    self.titleLabel.frame = CGRectMake(20.0f, 20.0f, kRBScreenWidth - 20.0f*2, [RBAutoSizeTableViewCell calculateTitleWidth:title]);
}
複製代碼

重寫UITableViewDataSource的protocol方法,動態計算每一行的高度:性能

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
    
    return [RBAutoSizeTableViewCell calculateTitleWidth:self.titles[indexPath.row]] + 20.0*2;
}
複製代碼

利用自動佈局和約束計算高度結合estimatedRowHeight特性計算高度

先將titleLabel利用約束固定在cell上:優化

[self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.left.mas_equalTo(self.contentView).with.mas_offset(20.0f);
        make.bottom.right.mas_equalTo(self.contentView).with.mas_offset(-20.0f);
}];
複製代碼

再將UITableView設置爲預估高度的模式:ui

self.estimatedRowHeight = 300.0f;  //設置近似值
self.rowHeight = UITableViewAutomaticDimension;
複製代碼

只須要兩行代碼,咱們就完成了動態高度的估算工做,很是的簡潔明瞭。

這裏我用了Xib加載cell和代碼構建cell兩種方式生成cell:

//代碼建立cell
if(!autoSizeTableViewCell){
        autoSizeTableViewCell = [[RBAutoSizeTableViewCell1 alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellID];
}

//nib建立cell
if(!autoSizeTableViewCell){
        autoSizeTableViewCell = [[NSBundle mainBundle] loadNibNamed:NSStringFromClass([RBAutoSizeTableViewCell2 class]) owner:self options:nil].lastObject;
}
複製代碼

儘管不少同窗都用過Xib文件,可是對於其中的原理不甚熟悉,Xib其實就是一個XML文件,在項目運行時會被編譯成二進制文件即nib文件,Fabric將會在下文中分析Xib的執行效率。

注意:千萬不要再次重寫- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath方法,不然UITableView將不會預估高度

加載效率對比

當我接到一個需求的時候,其實腦子裏面閃現過許多實現需求的方法,到底用哪種方法,取決於不少因素:代碼複雜度,可擴展性,穩定性,代碼執行效率等等。

今天Fabric主要從性能方面來分析兩種實現方式的優劣,下面是一張三種方式動態計算高度(咱們把Xib+約束動態計算單元格高度看成第三種自適應方法),加載UITableView所需時間的柱狀圖:

自適應高度耗時柱狀圖
固然,耗時的多少還和文本的大小有關係,Fabric爲了凸顯3種方法的效率差異故意把文本內容設置的很長。

正如你們看到的,代碼動態計算高度的耗時要遠遠地高於後二者,效率很是低下,當咱們把cell總數設置爲1000,甚至10000的時候,能夠很明顯的感覺到加載緩慢,嚴重的傷害了用戶體驗。

性能差異分析

你們可能會驚訝,短短几行代碼,爲何耗時的差距能夠高達上萬倍呢?!

緣由在於:當使用代碼動態計算高度時,UITableView會首先執行一遍

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
    
    return [RBAutoSizeTableViewCell calculateTitleWidth:self.titles[indexPath.row]] + 20.0*2;
}
複製代碼

方法,當有1000個cell的時候,UITableview就會首先執行1000次計算高度的方法,而後再去執行- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath獲取cell,獲取cell以後,又會執行一次- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath方法,來獲取當前cell的高度。這樣一來,確定要耗費很是長的時間。

反觀第二種方法,UITableView只會預加載一個UITableView contentSize的內容,也就是說,不管有多少cell,UITableView會先加載一屏內容,再預計算第二屏的高度,不會有更多的計算操做。這種預加載邏輯,保障了UITableView既不會卡頓,也不會消耗更多的資源。

另外看一下Xib+約束的執行效率,並不比純代碼要低,可能有讀者會有疑問:

  • 一、UITableView上一次性建立的Xib文件很少因此看不出性能差異。

  • 二、Xib文件上只有一個UILabel,太簡單了,因此看不出Xib文件的耗時。

因此Fabric把行高設置成5px,讓UITableView一次性多生成一些cell;儘可能多拖拽一些控件到Xib上,增長Xib文件的複雜度,執行結果顯示: 純代碼構建cell和用Xib獲取cell沒有明顯的性能區別。所以,Xib文件的執行效率是很高的,並不像我起先設想的那樣,讀取XML文件會很耗時。


總結

經過動態加載單元格的性能實驗,咱們知道了UITableView加載緩慢的緣由:重複執行了大量的耗時操做,所以Fabric總結了如下幾點提升UITableView加載效率的方法:

  • 一、不要在UITableViewDataSource的代理方法中加入過多的耗時方法,好比說計算寬高或者加載數據。
  • 二、儘可能複用自定義的UITableViewCell,而不是定義很是多個UITableViewCell,畢竟從緩存池裏獲取cell要比從新建立cell要來的快。
  • 三、對於須要反覆使用的數據建議加入緩存,好比說咱們要重複獲取一張名字爲"Fabric"的圖片,那麼咱們能夠用以下代碼:
- (UIImage *)getCellImage:(NSString *)imageName{
   
   if(!imageName) return nil;
   UIImage *img = [self.imageDict objectForKey:imageName];
   if(!img){
       img = [UIImage imageNamed:imageName];
       [self.imageDict setValue:img forKey:imageName];
   }
   return img;
}
複製代碼

固然,不管是第三方SDWebImage仍是系統方法+ (nullable UIImage *)imageNamed:(NSString *)name,都已經幫咱們將圖片存儲在磁盤上了,不須要咱們再次去作緩存了,Fabric只是用圖片緩存舉個例子而已。

  • 四、儘可能不要在- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath方法中,獲取到cell以後再去addSubView,若是這樣作的話,cell每一次出如今用戶界面上就add一次subView,那麼用戶來回滑動幾回UITableView,就會發現界面卡頓,滑動明顯變慢,甚至滑不動了。
  • 五、多用hidden屬性去隱藏對用戶不可見的控件,而不是經過設置alpha爲0,或者設置控件寬高爲0的方式來隱藏控件,由於當控件的hidden屬性爲YES的時候,系統會自動優化控件內存,減小設備的資源消耗。

後續 - 提升方法一(代碼計算高度)的UITableView的加載效率

首先,感謝兩位讀者Asuray和ControlM給Fabric的寶貴留言。他們一針見血的指出了代碼計算高度自適應UITableView效率低下的緣由:在UITableViewDataSource的代理方法中,執行了過多的冗餘的計算UILabel高度的操做

Fabric的方法一是一個不恰當的加載UITableView的思路,旨在讓讀者看到UITableView加載效率低下的緣由。下面咱們來設想一下如何優化,結合上文總結的五點提升UITableView加載效率的方法,Fabric想出瞭如下三點改進方法:

  • 1.加入緩存機制,即把title的內容寫入Model,在Model中計算出UILabel的高度,避免UITableView每次獲取高度都要計算一遍高度,也避免了在獲取到Cell的時候計算UILabel高度,代碼以下:
- (void)convertDataToModel{
    
    [self.titles enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        RBTitleModel *titleModel = [[RBTitleModel alloc] init];
        titleModel.title = obj;
        titleModel.titleLabelHeight = 0.0f;
        [self.titles replaceObjectAtIndex:idx withObject:titleModel];
    }];
}
複製代碼
  • 2.在- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath方法中再也不執行計算UILabel高度的方法,而是給出一個預設的高度,代碼以下:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
    
    RBTitleModel *model = [self.titles objectAtIndex:indexPath.row];
    return model.titleLabelHeight + 20.0*2;
}
複製代碼
  • 3.單純的把文字高度計算放入Model中效率仍是極其低下的,由於在reloadData以前,須要執行全部的Model的計算高度代碼,假設數據源是有1000000個元素的數組,那麼在轉換model以前,就須要計算1000000次高度。因此最好的方法是在- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath方法中計算UILabel的高度,代碼以下:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    
    static NSString *autoSizeTableViewCellID = @"RBAutoSizeTableViewCell";
    RBAutoSizeTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:autoSizeTableViewCellID];
    if(!cell){
        cell = [[RBAutoSizeTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:autoSizeTableViewCellID];
    }
    //動態計算當前cell的高度
    RBTitleModel *titleModel = [self.titles objectAtIndex:indexPath.row];
    [titleModel calculateTitleWidth];
    
    [cell buildData:self.titles[indexPath.row]];
    return cell;
}
複製代碼

在計算高度時,Fabric採用了緩存機制,若是titleModel.titleHeight的數值不爲0,說明已經計算太高度不須要重複計算,代碼以下:

- (void)calculateTitleWidth{
    //有緩存則不須要重複計算
    if(self.titleLabelHeight > 0) return;
    
    CGFloat stringWidth = 0;
    CGSize size = CGSizeMake(kRBScreenWidth - 20.0f*2, MAXFLOAT);
    
    if (self.title.length > 0) {
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000
        stringWidth = [self.title
                       boundingRectWithSize:size
                       options:NSStringDrawingUsesLineFragmentOrigin
                       attributes:@{NSFontAttributeName:kRBTextFont}
                       context:nil].size.height;
#else
        //iOS7.0如下方法
        stringWidth = [self.title sizeWithFont:kRBTextFont
                        constrainedToSize:size
                            lineBreakMode:NSLineBreakByCharWrapping].height;
#endif
    }
    self.titleLabelHeight = stringWidth;
}
複製代碼

通過改進以後,UITableView的執行效率明顯變高了,下圖是改進以後的兩種方式的UITableView加載耗時的柱狀圖:

雖然代碼計算高度的效率仍是最低的,可是相比以前要好了不少。感興趣的同窗能夠去個人GitHub上下載 Demo,閱讀源碼,也能夠本身動手實現一下。你們有更好的優化UITableView加載效率的方法,也能夠直接在Demo中修改,而後push給Fabric,你們共同進步,一塊兒提升技術水平。

Fabric能想到的優化UITableView加載效率的方法就只有以上這麼多了,歡迎你們在文章下方留言一塊兒探討,也能夠加個人微信justlikeitRobert和我討論,喜歡這篇文章請點贊,謝謝你們的關注與支持。

相關文章
相關標籤/搜索