如何優(yōu)化UITableView?

優(yōu)化UITableView常用的方式有:Cell重用、緩存Cell高度Cell數(shù)據(jù)資源緩存、渲染、減少視圖數(shù)目、減少多余的繪制操作不要給cell動(dòng)態(tài)添加subview、異步化UI,不要阻塞主線程滑動(dòng)時(shí)按需加載對應(yīng)的內(nèi)容。

1. Cell重用機(jī)制

這是UITableView的基本使用

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *Identifier = @"cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier: Identifier];
    if (!cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:ID];
    }
    return cell;
}

這樣并不完美,我們經(jīng)常在- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath中為每個(gè)cell綁定數(shù)據(jù),實(shí)際在調(diào)用- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath的時(shí)候cell還沒有被顯示出來。為了提高效率,我們應(yīng)該把數(shù)據(jù)綁定的操作放在cell即將顯示的時(shí)候執(zhí)行,即在- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath;方法中綁定數(shù)據(jù)。
注意- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath;在cell展示在tableView之前就會(huì)調(diào)用,此時(shí)cell的實(shí)例已經(jīng)生成,所以不能更改cell的結(jié)構(gòu),只能改動(dòng)cell上的UI的一些屬性,如:label的內(nèi)容等。

2. 緩存Cell高度

這里將Cell分成兩種:一種是定高的Cell,一種是動(dòng)態(tài)高度的Cell。

  1. 定高的Cell,應(yīng)該用如下的方式:
    self.tableView.rowHeight = 88;
    這個(gè)方法指定了所有cell的高度都是88,rowHeight默認(rèn)是44,對于定高的Cell直接使用該方法設(shè)定高度,不需要實(shí)現(xiàn)tableView:heightForRowAtIndexPath:以節(jié)省不必要的計(jì)算和開銷。
  2. 動(dòng)態(tài)高度的Cell
    我們需要實(shí)現(xiàn)tableView的代理,給出高度:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    //
    return xxx;
}

這個(gè)代理方法實(shí)現(xiàn)后,上面的rowHeight的設(shè)置將會(huì)變成無效。在這個(gè)方法中,我們需要提高Cell高度的計(jì)算效率,來節(jié)省時(shí)間。
自從iOS8之后,有了self-sizing Cell的概念,Cell可以自己算出高度,使用self-sizing cell需要滿足以下三個(gè)條件:
(1)使用AutoLayout進(jìn)行UI布局約束,要求cell.contentView的四條邊都與內(nèi)部元素有約束關(guān)系;
(2)指定TableView的estimatedRowHeight屬性的默認(rèn)值;
(3)指定TableView的rowHeight屬性為UITableViewAutomaticDimension;

- (void)viewDidLoad {
    self.tableView.estimatedRowHeight = 44.0;
    self.tableView.rowHeight = UITableViewAutomaticDimension;
}

除了提高Cell的計(jì)算效率之外,對于已經(jīng)計(jì)算出的高度,我們需要進(jìn)行緩存,對于已經(jīng)計(jì)算過的高度,沒有必要計(jì)算第二次。
創(chuàng)建viewModel,計(jì)算并存儲(chǔ)Cell的UI尺寸信息

@interface YZZYCellHeightViewModel : NSObject
@property (strong, nonatomic) YZZYModel *dataModel; // 原始數(shù)據(jù)模型
@property (assign, nonatomic) CGFloat cellHeight; // Cell高度
- (void)calculateCellHeight; // 計(jì)算高度

這里需要注意:
在iOS中,系統(tǒng)是先調(diào)用- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath獲取每個(gè)Cell即將顯示的高度,確定整個(gè)UITableView的布局,然后才調(diào)用- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath獲取Cell。因此,使用了ViewModel來保存UI信息,Cell高度的計(jì)算和使用時(shí)機(jī)需要特別留意。

3. Cell數(shù)據(jù)資源緩存

提前處理Cell需要顯示的數(shù)據(jù)資源,在Cell顯示之前,將從服務(wù)器加載獲取到的原始數(shù)據(jù)在viewModel中提前處理,一般包括:圖片的加載和壓縮、富文本的多樣化顯示(NSString->NSAttributeString).
這時(shí)的viewModel可能是這樣的

@interface YZZYViewModel : NSObject
@property (strong, nonatomic) YZZYModel *dataModel; //原始數(shù)據(jù)模型
@property (assign, nonatomic) CGFloat cellHeight; // Cell高度
/* 需要展示的內(nèi)容*/
@property (strong, nonatomic) NSAttributeString *titleToShow;
@property (strong, nonatomic) NSAttributeString *contentToShow;
- (void)calculateCellHeight; // 計(jì)算高度
- (void)handleSourceDataModel;
@end

4. 渲染

為了保證UITableView的流暢,當(dāng)快速滑動(dòng)的時(shí)候,Cell必須被快速的渲染出來。提高Cell渲染速度的方法有以下三種:

  1. 當(dāng)有圖像時(shí),預(yù)渲染圖像,在bitmap context先將其畫一遍,導(dǎo)出成UIImage對象,然后再繪制到屏幕,這會(huì)大大提高渲染速度。具體內(nèi)容可以自行查找“利用預(yù)渲染加速顯示iOS圖像”相關(guān)資料。
  2. 渲染最耗時(shí)的操作之一就是混合(blending),所以不要使用透明背景,將Cell的opaque(不透明)的值設(shè)為YES,背景色不要使用clearColor,盡量不要使用陰影漸變等。
  3. 由于混合操作是使用GPU來執(zhí)行,我們可以用CPU來渲染,這樣混合操作就不再執(zhí)行,可以在UIView的drawRect方法中自定義繪制。

5. 減少視圖的數(shù)目

我們在Cell上添加系統(tǒng)控件的時(shí)候,實(shí)際上系統(tǒng)都會(huì)調(diào)用底層接口進(jìn)行繪制,大量添加控件時(shí),會(huì)消耗很大的資源并且也會(huì)影響渲染的性能。當(dāng)使用默認(rèn)的UITableViewCell并且在它的ContentView上面添加控件時(shí)會(huì)相當(dāng)消耗性能。所以,目前最佳的方法還是繼承UITableViewCell,并重寫drawRect方法。

5. 減少多余的繪制操作

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

6. 不要給Cell動(dòng)態(tài)添加subView

在初始化Cell的時(shí)候就將所有需要展示的控件添加完畢,然后根據(jù)需要來設(shè)置hide屬性顯示和隱藏控件。

7. 異步化UI,不要阻塞主線程

加載時(shí)整個(gè)頁面卡住不動(dòng),怎么點(diǎn)都沒用,仿佛死機(jī)了一般,原因就是主線程被阻塞了。所以,對于網(wǎng)絡(luò)請求或圖片的加載,可以開啟多線程將耗時(shí)的操作放在子線程中進(jìn)行,異步操作。

8. 滑動(dòng)時(shí)按需加載對應(yīng)的內(nèi)容

如果目標(biāo)行與當(dāng)前行相差超過指定行數(shù),只在目標(biāo)滾動(dòng)范圍的前后制定3行加載。

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inoutCGPoint *)targetContentOffset
{
    NSIndexPath *ip = [self indexPathForRowAtPoint:CGPointMake(0, targetContentOffset->y)];
    
    NSIndexPath *cip = [[self indexPathsForVisibleRows] firstObject];
    NSInteger skipCount = 8;
    if( labs(cip.row - ip.row) > skipCount) {
        NSArray *temp = [self indexPathsForRowsInRect:CGRectMake(0, targetContentOffset->y, self.width, self.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]];
            }
        }
        [needLoadArr addObjectsFromArray:arr];
    }
}

記得在tableView:cellForRowAtIndexPath:方法中加入判斷

if(needLoadArr.count > 0 && [needLoadArr indexOfObject:indexPath] == NSNotFound)
{
    [cell clear];
    return;
}

9. 離屏渲染

  1. 以下的操作會(huì)引發(fā)離屏渲染:
    (1)為圖層設(shè)置遮罩(layer.mask)
    (2)將圖層的layer.maskToBounds/view.clipsToBounds屬性設(shè)置為true
    (3)將圖層layer.allowsGroupOpacity屬性設(shè)置為YES和layer.opacity小于1.0
    (4)將圖層設(shè)置陰影(layer.shadow)
    (5)為圖層設(shè)置layer.shouldRasterize=true
    (6)具有l(wèi)ayer.cornerRadius、layer.edgeAntialiasingMask、layer.allowsEdgeAntialiasing的圖層
    (7)任何種類的文本,包括:UILabel、CATextLayer和Core Text等
    (8)使用CGContext在drawRect:方法中繪制大部分情況下會(huì)導(dǎo)致離屏渲染,甚至僅僅是一個(gè)空的實(shí)現(xiàn)。
  2. 優(yōu)化方案
    官方對離屏渲染產(chǎn)生的性能問題也進(jìn)行了優(yōu)化,iOS 9.0之前UIImageView跟UIButton設(shè)置圓角都會(huì)觸發(fā)離屏渲染;iOS 9.0之后UIButton設(shè)置圓角會(huì)觸發(fā)離屏渲染,而UIImageView里png圖片設(shè)置圓角不會(huì)觸發(fā)離屏渲染了,如果設(shè)置其他陰影效果之類的還是會(huì)觸發(fā)離屏渲染。
    (1)圓角優(yōu)化
    在APP開發(fā)中,圓角優(yōu)化經(jīng)常出現(xiàn)。如果一個(gè)界面中只有少量圓角圖片或許對性能沒有非常大的影響,但是當(dāng)圓角圖片比較多的時(shí)候就會(huì)對APP的性能產(chǎn)生明顯的影響。
    開發(fā)者設(shè)置圓角一般通過如下方式:
imageView.layer.cornerRadius = CGFloat(10);
imageView.layer.masksToBounds = YES;

這樣處理的渲染機(jī)制是GPU在當(dāng)前屏幕緩沖區(qū)外新開辟一個(gè)渲染緩沖區(qū)進(jìn)行工作,也就是離屏渲染,這會(huì)給我們帶來額外的性能損耗。如果這樣的圓角操作達(dá)到一定數(shù)量,會(huì)觸發(fā)緩沖區(qū)的頻繁合并和上下文的頻繁切換,性能的代價(jià)體現(xiàn)在用戶體驗(yàn)上就是掉幀。
優(yōu)化方案1:使用貝塞爾曲線UIBezierPath和Core Graphics框架畫出一個(gè)圓角

UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
imageView.image = [UIImage imageNamed:@"myImg"];
// 開始對imageView進(jìn)行畫圖
UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, NO, 1.0);
// 使用貝塞爾曲線畫出一個(gè)圓形圖
[[UIBezierPath bezierPathWithRoundedRect:imageView.bounds cornerRadius:imageView.frame.size.width] addClip];
[imageView drawRect:imageView.bounds];
imageView.image = UIGraphicsGetImageFromCurrentImageContext();
// 結(jié)束畫圖
UIGraphicsEndImageContext();
[self.view addSubview:imageView];

優(yōu)化方案2:使用CAShapeLayer和UIBezierPath設(shè)置圓角

UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
imageView.image = [UIImage imageNamed:@"myImg"];
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;
[self.view addSubview:imageView];

對于方案2需要解釋的是:

  • CAShapeLayer繼承于CALayer,可以使用CALayer的所有屬性;
  • CAShapeLayer需要貝塞爾曲線配合使用才有效果;
  • 使用CAShapeLayer(屬于Core Animation)與貝塞爾曲線可以實(shí)現(xiàn)不在view的drawRect方法中畫出一些想要的圖片;
  • CAShapeLayer動(dòng)畫渲染直接提交到手機(jī)的GPU中,相較于view的drawRect方法使用CPU渲染而言,其效率極高,能大大優(yōu)化內(nèi)存使用情況。
    總的來說就是CAShapeLayer的內(nèi)存消耗較少,渲染速度快,建議使用優(yōu)化方案2。
    (2)shadow優(yōu)化
    對于shadow,如果圖層是個(gè)簡單的幾何圖形或者圓角圖形,我們可以通過設(shè)置shadowPath來優(yōu)化性能,能大幅提高性能。示例如下:
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;

開發(fā)者可以通過設(shè)置shouldRasterize屬性為YES來強(qiáng)制開啟離屏渲染,其實(shí)就是光柵化(Rasterization)。
既然離屏渲染這么不好,為什么還要強(qiáng)制開啟呢?
當(dāng)一個(gè)圖像混合了多個(gè)圖層,每次移動(dòng)時(shí),每一幀都要重新合成這些圖層,十分消耗性能。當(dāng)開啟光柵化后,會(huì)在首次產(chǎn)生一個(gè)位圖緩存,當(dāng)再次使用時(shí)就會(huì)復(fù)用這個(gè)緩存。但是如果圖層發(fā)生改變的時(shí)候就會(huì)重新產(chǎn)生位圖緩存。所以這個(gè)功能一般不能用于UITableViewCell中,Cell的復(fù)用反而降低了性能。最好用于圖層較多的靜態(tài)內(nèi)容的圖形。而且產(chǎn)生的位圖緩存大小是有限制的,一般是2.5個(gè)屏幕尺寸。在100ms之內(nèi)不使用這個(gè)緩存,緩存也會(huì)被刪除,所以我們要根據(jù)使用場景而定。
(3)其他一些優(yōu)化建議

  • 當(dāng)我們需要圓角效果時(shí),可以使用一張中間透明的圖片蒙上去;
  • 使用ShadowPath指定layer陰影效果路徑
  • 使用異步進(jìn)行l(wèi)ayer渲染(Facebook開源的異步繪制框架AsyncDisplayKit)
  • 設(shè)置layer的opaque值為YES,減少復(fù)雜圖層合成
  • 盡量使用不包含透明(alpha)通道的圖片資源
  • 盡量設(shè)置layer的大小值為整型值
  • 直接讓美工把圖片切成圓角進(jìn)行顯示,這是效率最高的一種方案
  • 很多情況下,用戶上傳圖片進(jìn)行顯示,可以讓服務(wù)端處理圓角
  • 使用代碼手動(dòng)生成圓角Image設(shè)置到要顯示的view上,利用UIBezierPath(Core Graphics框架)畫出圓角圖片
    (4)Core Animation工具檢測離屏渲染
    對于離屏渲染的監(jiān)測,蘋果為我們提供了一個(gè)測試工具Core Animation,可以在Xcode->Open Developer Tools->Instruments中找到,如下圖:

    Core Animation工具用來監(jiān)測Core Animation性能,提供可見的FPS值,并且提供幾個(gè)選項(xiàng)來測量渲染性能,如下圖:

    下面我們來說明每個(gè)選項(xiàng)的功能
  • Color Blended Layers:這個(gè)選項(xiàng)如果勾選,你能看到哪個(gè)layer是透明的,GPU正在做混合計(jì)算。顯示紅色的就是透明的,綠色就是不透明的。
  • Color Hits Green and Misses Red:如果勾選你這個(gè)選項(xiàng),且當(dāng)我們代碼中有設(shè)置shouldRasterize為YES,那么紅色代表沒有復(fù)用離屏渲染的緩存,綠色則表示復(fù)用了緩存,開發(fā)者當(dāng)然希望能夠復(fù)用。
  • Color Copied Images:按照官方的說法,當(dāng)圖片的顏色格式GPU不支持的時(shí)候,Core Animation會(huì)拷貝一份數(shù)據(jù)讓CPU進(jìn)行轉(zhuǎn)化。例如:從網(wǎng)絡(luò)上下載了TIFF格式的圖片,則需要CPU進(jìn)行轉(zhuǎn)化,這個(gè)區(qū)域會(huì)顯示成藍(lán)色。還有一種情況會(huì)觸發(fā)Core Animation的copy方法,就是字節(jié)不對齊的時(shí)候,如下圖:
  • Color Immediately:默認(rèn)情況下Core Animation工具以每毫秒10次的頻率更新圖層調(diào)試顏色,如果勾選這個(gè)選項(xiàng)則移除10ms的延遲。對某些情況需要這樣,但是有可能影響正常幀數(shù)的測試。
  • Color Misaligned Images:勾選此項(xiàng),如果圖片需要縮放則標(biāo)記為黃色,如果沒有像素對齊則標(biāo)記為紫色。像素對齊我們已經(jīng)在上面有所介紹。
  • Color Offscreen-Rendered Yellow:用來檢測離屏渲染的,如果顯示黃色,表示有離屏渲染。當(dāng)然還要結(jié)合Color Hits Green and Misses Red來看,是否復(fù)用了緩存。
  • Color OpenGL Fast Path Blue:這個(gè)選項(xiàng)對那些使用OpenGL的圖層才有用,像是GLKView或者 CAEAGLLayer,如果不顯示藍(lán)色則表示使用了CPU渲染,繪制在了屏幕外,顯示藍(lán)色表示正常。
  • Flash Updated Regions:當(dāng)對圖層重繪的時(shí)候回顯示黃色,如果頻繁發(fā)生則會(huì)影響性能??梢杂迷黾泳彺鎭碓鰪?qiáng)性能。

其他

  1. Cell中的View盡量不使用透明
  2. 減少子視圖的層級(jí)關(guān)系
  3. 圖片載入在后臺(tái)進(jìn)程進(jìn)行,滾出可視范圍的載入進(jìn)程cancel掉
  4. 圖片資源盡可能使用PNG

參考來源:

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對...
    cosWriter閱讀 11,683評(píng)論 1 32
  • 1.ios高性能編程 (1).內(nèi)層 最小的內(nèi)層平均值和峰值(2).耗電量 高效的算法和數(shù)據(jù)結(jié)構(gòu)(3).初始化時(shí)...
    歐辰_OSR閱讀 30,282評(píng)論 8 265
  • 1、通過CocoaPods安裝項(xiàng)目名稱項(xiàng)目信息 AFNetworking網(wǎng)絡(luò)請求組件 FMDB本地?cái)?shù)據(jù)庫組件 SD...
    陽明AI閱讀 16,228評(píng)論 3 119
  • 鷹飛翔在大海上空, 聽海浪的咆哮; 鷹落在礁石上, 看亂石穿空的暴躁。 它血液里是桀驁不馴的遺傳, 它血液里是凌駕...
    趙建銅閱讀 525評(píng)論 0 1
  • 薄暮煙村雨,孤云萬里風(fēng)。別來簫管惹飛鴻。鐵馬春秋幾度?且共從戎。 曉箭雕弓響,流年類轉(zhuǎn)蓬。旅思夤夜枕香夢。兵作虬爐...
    幽小窗閱讀 652評(píng)論 21 35

友情鏈接更多精彩內(nèi)容