【iOS】性能優(yōu)化總結(jié) —— UITableView

優(yōu)化一、善用重用標(biāo)識

這個屬于基礎(chǔ)知識范疇,就不再過度的講解了。只需了解使用 static 修飾重用標(biāo)識名稱能夠保證這個標(biāo)識只會創(chuàng)建一次,提高性能。接著就是調(diào)用dequeueReusableCellWithIdentifier:方法獲取緩存池中的Cell。如果沒有就調(diào)用 initWithStyle:ReusIdentifier:方法創(chuàng)建一個新的Cell。注意事先需要調(diào)用registerNib/registerClass方法為TableView注冊一下重用標(biāo)識。

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];  
    if (!cell) {  
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];  
    } 

需要注意的是我們在自定義Cell的時候需要盡量將相識度高的Cell合并為一種樣式的Cell。我們知道運行在當(dāng)前設(shè)備屏幕中的Cell數(shù)量是有限的,設(shè)為N個。如果有M種樣式,那么基于Cell的重用機制,我們知道在緩存池中運行最多時將有M*N個Cell的實例被創(chuàng)建,這顯然會占用大量的內(nèi)存。

優(yōu)化二、設(shè)置預(yù)估行高,預(yù)先緩存動態(tài)行高

1. 設(shè)置預(yù)估行高

我們知道UITableView是通過設(shè)置UITableView代理方法heightForRowAtIndexPath:方法來設(shè)置行高。自從iOS8.0之后,蘋果新增了self-sizing cell的概念,也是cell可以自己計算行高,使用需要滿足三個條件:
(1) 使用Autolayout進行UI布局約束
(2) 指定TableView的estimatedRowHeight屬性的默認(rèn)值
(3) 指定TableView的rowHeight的屬性為UITableViewAutomaticDimension

TableView在加載數(shù)據(jù)時會先通過estimatedHeightForRowAtIndexPath處理全部數(shù)據(jù),此時我們只需要提供一個粗略的高度,待到cell對象創(chuàng)建之后再去設(shè)置cell的真實高度。而且只會處理當(dāng)前屏幕范圍內(nèi)的cell,這樣子會顯著的提升加載的性能。

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {  
    return 50.0;  
}  
  
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {  
    return 30.0;  
}  
2. 預(yù)先計算并緩存行高

自從iOS8.0之后,TableView的數(shù)據(jù)源的調(diào)用時序也發(fā)生了變化,下圖左邊為iOS7.0及之前的時序,右邊為iOS8.0及之后的時序:

9073B542-D111-4D3C-A89F-B8AB87B4EF27.png

從上圖我們可以很容易的分析出,iOS8.0之后再獲取cell對象之后會再次調(diào)用heightForRowAtIndexPath: 方法獲取行高,這也就意味著我們其實可以先創(chuàng)建cell對象,之后再提供行高。具體方法我們可以在cell類中添加layoutAttribute屬性,記錄相應(yīng)的UIEdgeInsets,然后在設(shè)置cell真實高度的時候返回。iOS7.0之前則是必須在cell對象創(chuàng)建之前先獲得所有Cell的高度。

優(yōu)化三、減少Subviews層級、異步繪制、避免離屛渲染、使用hidden隱藏圖層

1. 減少圖層層級數(shù)

當(dāng)我們自定義了某個cell,并在cell上添加大量的系統(tǒng)控件后,在創(chuàng)建該cell對象時系統(tǒng)會調(diào)用底層接口進行繪制,大量的添加操作會消耗很大的資源同時會影響渲染的性能。

2. 異步繪制

解決因圖層層級多造成的性能問題,我們可以通過重寫drawReact:方法,調(diào)用Core Graphics框架中的API進行異步繪制,提高效率。drawRect:本身是異步的。另外drawRect:中大量的繪制操作也會造成內(nèi)存的增長,可以使用CAShapeLayer來代替。

3. 減少多于的繪制操作

在實現(xiàn)drawRect:方法的時候,他的參數(shù)rect就是我們需要繪制的區(qū)域,在rect范圍之外的區(qū)域不要繪制,否則會消耗相當(dāng)大的資源。

4. 圖片加載時機選擇

首先在Cell類中添加圖片應(yīng)該避免使用imageWithName:方法,因為該方法會將圖片緩存到內(nèi)存中。而是應(yīng)該使用imageWithContensOfFile:方法來替換,該方法在圖片使用完后系統(tǒng)會自動釋放資源,并不會緩存下來。另外結(jié)合SDWebImage框架的使用可以顯著的提高圖片加載的性能。

5. 避免動態(tài)添加圖層

在cell中應(yīng)該盡量避免動態(tài)創(chuàng)建圖層。在初始化cell的時候一并將所有圖層預(yù)先創(chuàng)建好,通過hidden屬性控制子圖層的顯示或隱藏,因為單純的顯示操作要比創(chuàng)建快的多。

6. 避免離屛渲染
  • 為圖層設(shè)置遮罩(layer.mask
  • 設(shè)置圖層的 layer.masksToBounds/view.clipsToBounds屬性為True
  • 設(shè)置圖層的 layer.allowsGroupOpacity的屬性為True和layer.opacity小于1.0
  • 設(shè)置圖層陰影(layer.shadow
  • 設(shè)置圖層的 layer.shouldRasterize的屬性為True
  • 具有 layer.cornerRadius, layer.edgeAntialiasingMask, layer.allowsAntialiasing的圖層
  • 文本(任何種類,包括UILabelCATextLayerCore Text等)
  • 使用CGContextdrawReact:方法中繪制
    上述情況均會造成離屛渲染。

什么是離屛渲染?我們知道iOS底層的渲染框架使用的是OpenGL ESOpenGL中,GPU渲染屏幕方式有兩種:當(dāng)前屏幕渲染(On-Screen Rendering)和離屛渲染(Off-Screen Rendering)。它們的區(qū)別是當(dāng)前屏幕渲染操作是在當(dāng)前顯示的屏幕緩沖區(qū)完成,而離屛渲染會在另外一個新開辟的緩沖區(qū)完成渲染操作。開啟離屛渲染的代價就是需要新開辟一塊新的緩沖區(qū),在渲染的過程中還會多次的切換上下文,這些都是很消耗性能的。

7. 圖片圓角優(yōu)化
  • 使用貝塞爾曲線 + Core Graphics框架設(shè)置圓角
- (void)setImageCircularEdge:(UIImageView *)imageView {  
      
    //開始對imageView進行畫圖  
    UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, NO, 1.0);  
    //使用貝塞爾曲線畫出一個圓形圖  
    [[UIBezierPath bezierPathWithRoundedRect:imageView.bounds cornerRadius:imageView.frame.size.width] addClip];  
    [imageView drawRect:imageView.bounds];  
    imageView.image = UIGraphicsGetImageFromCurrentImageContext();  
    //結(jié)束畫圖  
    UIGraphicsEndImageContext();  
}  
  • 使用貝塞爾曲線 + CAShapeLayer 設(shè)置圓角
- (void)setImageCircularEdge2:(UIImageView *)imageView {  
  
    UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:imageView.bounds byRoundingCorners:UIRectCornerAllCorners cornerRadii:imageView.bounds.size];  
    CAShapeLayer *maskLayer=[[CAShapeLayer alloc] init];  
    //設(shè)置大小  
    maskLayer.frame = imageView.bounds;  
    //設(shè)置圖形樣子  
    maskLayer.path = maskPath.CGPath;  
    imageView.layer.mask = maskLayer;  
}  
8. 圖片陰影優(yōu)化
- (void)setImageShadow:(UIImageView *)imageView {  
      
    imageView.layer.shadowColor = [UIColor grayColor].CGColor;  
    imageView.layer.shadowOpacity = 1.0;  
    imageView.layer.shadowRadius = 2.0;  
    UIBezierPath *path=[UIBezierPath bezierPathWithRect:imageView.frame];  
    imageView.layer.shadowPath = path.CGPath;  
}  

優(yōu)化四、分屏加載數(shù)據(jù),預(yù)先異步請求數(shù)據(jù)

在我們的項目開發(fā)中列表視圖的應(yīng)用很多,有時數(shù)據(jù)比較多的時候我們不可能一次加載所有數(shù)據(jù),這樣子會導(dǎo)致內(nèi)存的暴漲,同時用戶不一定會瀏覽完所有的信息,造成資源浪費。這時我們可以通過分屏加載來解決這個問題,比如第一次加載10條數(shù)據(jù),當(dāng)我向上滑動列表的時候通常我們會再次去請求數(shù)據(jù)接口獲取下一個10條數(shù)據(jù)。這個時候如果我們不做任何的處理,那么我們會發(fā)現(xiàn)每次劃過10條數(shù)據(jù)的時候列表都需要停頓一下,等待數(shù)據(jù)加載。這樣子我們的列表就表現(xiàn)的不是很流暢了,怎么解決這個問題呢?

提前異步預(yù)加載數(shù)據(jù)!第一次加載完10條數(shù)據(jù)之后我可以再預(yù)先加載下10條數(shù)據(jù),當(dāng)劃過第10條數(shù)據(jù)時,我再請求下下10條數(shù)據(jù)。這樣子我們的列表就表現(xiàn)的很流暢了。

優(yōu)化五、滑動TableView時,按需加載內(nèi)容

有些情況下我們可能會去快速的滑動列表,這時候其實會有大量的cell對象被創(chuàng)建、被重用,其實我們可能只是去瀏覽列表停止的那一頁的上下一定范圍內(nèi)的信息,前面快速劃過的那些信息對我們來說都是無用的。有什么方法讓我們只去加載我最后那頁的目標(biāo)范圍內(nèi)的列表數(shù)據(jù)呢?那就是通過ScrollView的代理方法scrollViewWillEndDragging:withVelocity:targetContentoffset:來實現(xiàn)的。

#pragma mark - UIScrollViewDelegate  
//按需加載 - 如果目標(biāo)行與當(dāng)前行相差超過指定行數(shù),只在目標(biāo)滾動范圍的前后指定3行加載。  
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {  
      
    NSIndexPath *targetPath = [_myTableView indexPathForRowAtPoint:CGPointMake(0, targetContentOffset->y)];  
    NSIndexPath *firstVisiblePath = [[_myTableView indexPathsForVisibleRows] firstObject];  
    NSInteger skipCount = 8;  
    if (labs(firstVisiblePath.row - targetPath.row)>  skipCount) {  
        NSArray *temp = [_myTableView indexPathsForRowsInRect:CGRectMake(0, targetContentOffset->y, _myTableView.frame.size.width, _myTableView.frame.size.height)];  
        NSMutableArray *arr = [NSMutableArray arrayWithArray:temp];  
        if (velocity.y<0) {  
            NSIndexPath *indexPath = [temp lastObject];  
            if (indexPath.row+33) {  
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row-3 inSection:0]];  
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row-2 inSection:0]];  
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row-1 inSection:0]];  
            }  
        }  
        [_dataList addObjectsFromArray:arr];  
    }  
}  

targetContentOffset 是TableView減速到停止的地方, velocity 表示速度向量。

優(yōu)化六、Cell類中應(yīng)該避免請求網(wǎng)絡(luò)加載數(shù)據(jù)

如果確實有需求不可避免,可以將網(wǎng)絡(luò)加載任務(wù)添加到Runloop中,設(shè)置DefaultRunloopModule模式。這樣子可以起到延遲加載的作用。

優(yōu)化七、在willDisplayCell:forRowAtIndexPath:代理方法中綁定數(shù)據(jù)

初學(xué)iOS的時候,各類教程以及書籍中都喜歡在cellForRowAtIndexPath:方法中綁定數(shù)據(jù),然后此時的Cell其實還未顯示,該方法中包含了大量的布局、繪制相關(guān)的操作。我們應(yīng)該在該方法中盡量簡化我們自身的邏輯操作。這時我們可以使用在willDisplayCell:forRowAtIndePath:方法中綁定數(shù)據(jù)。

#pragma mark - UITableViewDataSource  
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {  
    static NSString *cellIdentifier = @"MyTableViewCell";  
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];  
    if (!cell) {  
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];  
    }  
      
    return cell;  
}  
  
#pragma mark - UITableViewDelegate  
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {  
      
    NSDictionary *dict = self.dataList[indexPath];  
    [cell updateData:dict];  
}  
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容