UITableView的復(fù)用機(jī)制以及性能優(yōu)化

UITableView的復(fù)用機(jī)制

UITableView首先加載一屏幕(假設(shè)UITableView的大小是整個(gè)屏幕的大小)所需要的UITableViewCell,具體個(gè)數(shù)要根據(jù)每個(gè)cell的高度而定,總之肯定要鋪滿整個(gè)屏幕,更準(zhǔn)確說當(dāng)前加載的cell的高度要大于屏幕高度。然后你往上滑動,想要查看更多的內(nèi)容,那么肯定需要一個(gè)新的cell放在已經(jīng)存在內(nèi)容的下邊。這時(shí)候先不去生成,而是先去UITableView自己的一個(gè)資源池里去獲取。這個(gè)資源池里放了已經(jīng)生成的而且能用的cell。如果資源池是空的話才會主動生成一個(gè)新的cell。那么這個(gè)資源池里的cell又來自哪里呢?當(dāng)你滑動時(shí)視圖是,位于最頂部的cell會相應(yīng)的往上滑動,直到它徹底消失在屏幕上,消失的cell去了哪里呢?你肯定想到了,是的,它被UITableView放到資源池里了。其他cell也是這樣,只要一滑出屏幕就放入資源池。這樣,有進(jìn)有出,總共需要大約一屏幕多一兩個(gè)的cell就夠了。相對于1000來說節(jié)省的資源就是指數(shù)級啊,完美解決了性能問題。

常用的代碼

//方法1
- (void)viewDidLoad { 
    [super viewDidLoad]; 
        // Setup table view. 
    self.myTableView.delegate = self;
    self.myTableView.dataSource = self;
   [self.myTableView registerClass:[MyTableViewCell class] forCellReuseIdentifier:@"MyTableViewCell"];
   //或者[UITableView registerNib:forCellReuseIdentifier:]方法
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
     static NSString *CellIdentifier = @"MyTableViewCell"; 
     UITableViewCell *cell = nil; 
     cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];

    //do something
    return cell;
}
//方法2
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 
    static NSString *CellIdentifier = @"UITableViewCell"; 
   UITableViewCell *cell = nil; 
   cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; 
   if (!cell) { 
       cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
      //不要在這里設(shè)置cell的屬性
   } 
   //do something
   return cell;
}

注意這里- [UITableView registerClass:forCellReuseIdentifier:]- [UITableView registerNib:forCellReuseIdentifier:]這兩個(gè)方法一定不能用錯(cuò),否則就會報(bào)錯(cuò)。
還有就是我注釋中說的不要在if里面設(shè)置cell的屬性,這就是因?yàn)樗膹?fù)用機(jī)制,如果在那里設(shè)置了,你后面的cell因?yàn)槭菑?fù)用的前面的cell,所以不會執(zhí)行if里面的代碼,就會導(dǎo)致你設(shè)置的屬性失效。

UITableView的優(yōu)化

1、cell高度的計(jì)算

如果是固定高度,則直接設(shè)置self.tableView.rowHeight = 88;,不要重寫-(CGFloat)tableView:(UITableView *)tableViewheightForRowAtIndexPath:(NSIndexPath *)indexPath這個(gè)方法,重寫了這個(gè)方法后前面設(shè)置的rowHeight將會失效,并且每次顯示一個(gè)cell都會調(diào)用一次這個(gè)方法,所以重寫這個(gè)方法肯定沒有直接設(shè)置rowHeight效率高。
iOS8之后有了self-sizing cell的概念,cell可以自己算出高度,使用self-sizing cell需要滿足以下三個(gè)條件:

  • 使用Autolayout進(jìn)行UI布局約束(要求cell.contentView的四條邊都與內(nèi)部元素有約束關(guān)系)。
  • 指定TableView的estimatedRowHeight屬性的默認(rèn)值,就是初始化的一個(gè)默認(rèn)高度。
  • 指定TableView的rowHeight屬性為UITableViewAutomaticDimension。
- (void)viewDidload {
    self.myTableView.estimatedRowHeight = 44.0;
    self.myTableView.rowHeight = UITableViewAutomaticDimension;
}

除了提高cell高度的計(jì)算效率之外,對于已經(jīng)計(jì)算出的高度,也可以進(jìn)行緩存,對于已經(jīng)計(jì)算過的高度,沒有必要進(jìn)行計(jì)算第二次。

2、渲染

GPU渲染機(jī)制:

CPU 計(jì)算好顯示內(nèi)容提交到 GPU,GPU 渲染完成后將渲染結(jié)果放入幀緩沖區(qū),隨后視頻控制器會按照 VSync 信號逐行讀取幀緩沖區(qū)的數(shù)據(jù),經(jīng)過可能的數(shù)模轉(zhuǎn)換傳遞給顯示器顯示。

GPU屏幕渲染有以下兩種方式:
  • On-Screen Rendering
    意為當(dāng)前屏幕渲染,指的是GPU的渲染操作是在當(dāng)前用于顯示的屏幕緩沖區(qū)中進(jìn)行。
  • Off-Screen Rendering
    意為離屏渲染,指的是GPU在當(dāng)前屏幕緩沖區(qū)以外新開辟一個(gè)緩沖區(qū)進(jìn)行渲染操作。
離屏渲染的代價(jià)

相比于當(dāng)前屏幕渲染,離屏渲染的代價(jià)是很高的,主要體現(xiàn)在兩個(gè)方面:

  • 創(chuàng)建新緩沖區(qū)

要想進(jìn)行離屏渲染,首先要創(chuàng)建一個(gè)新的緩沖區(qū)。

  • 上下文切換

離屏渲染的整個(gè)過程,需要多次切換上下文環(huán)境:先是從當(dāng)前屏幕(On-Screen)切換到離屏(Off-Screen);等到離屏渲染結(jié)束以后,將離屏緩沖區(qū)的渲染結(jié)果顯示到屏幕上又需要將上下文環(huán)境從離屏切換到當(dāng)前屏幕。而上下文環(huán)境的切換是要付出很大代價(jià)的。

總之:離屏渲染會付出很大的開銷,能避免離屏渲染盡量就不要離屏渲染

下面的情況或操作會引發(fā)離屏渲染:
  • 設(shè)置透明(alpha)屬性
  • 為圖層設(shè)置遮罩(layer.mask)
  • 將圖層的layer.masksToBounds / view.clipsToBounds屬性設(shè)置為true
  • 將圖層layer.allowsGroupOpacity屬性設(shè)置為YES和layer.opacity小于1.0
  • 為圖層設(shè)置陰影(layer.shadow *)。
  • 為圖層設(shè)置layer.shouldRasterize=true
  • 具有l(wèi)ayer.cornerRadius,layer.edgeAntialiasingMask,layer.allowsEdgeAntialiasing的圖層
  • 文本(任何種類,包括UILabel,CATextLayer,Core Text等)使用CGContext在drawRect :方法中繪制大部分情況下會導(dǎo)致離屏渲染,甚至僅僅是一個(gè)空的實(shí)現(xiàn)

iOS 9.0 之前UIimageView跟UIButton設(shè)置圓角都會觸發(fā)離屏渲染。
iOS 9.0 之后UIButton設(shè)置圓角會觸發(fā)離屏渲染,而UIImageView里png圖片設(shè)置圓角不會觸發(fā)離屏渲染了,如果設(shè)置其他陰影效果之類的還是會觸發(fā)離屏渲染的。

常用的幾個(gè)優(yōu)化
1、圓角優(yōu)化

在APP開發(fā)中,圓角圖片還是經(jīng)常出現(xiàn)的。如果一個(gè)界面中只有少量圓角圖片或許對性能沒有非常大的影響,但是當(dāng)圓角圖片比較多的時(shí)候就會APP性能產(chǎn)生明顯的影響。
我們設(shè)置圓角一般通過如下方式:

imageView.layer.cornerRadius=CGFloat(10);
imageView.layer.masksToBounds=YES;

優(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/2.0] 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(屬于CoreAnimation)與貝塞爾曲線可以實(shí)現(xiàn)不在view的drawRect(繼承于CoreGraphics走的是CPU,消耗的性能較大)方法中畫出一些想要的圖形
  • CAShapeLayer動畫渲染直接提交到手機(jī)的GPU當(dāng)中,相較于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.bounds];
imageView.layer.shadowPath=path.CGPath;

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

3、其他的一些優(yōu)化建議
  • 當(dāng)我們需要圓角效果時(shí),可以使用一張中間透明圖片蒙上去
  • 使用ShadowPath指定layer陰影效果路徑
  • 使用異步進(jìn)行l(wèi)ayer渲染(Facebook開源的異步繪制框架AsyncDisplayKit)
  • 設(shè)置layer的opaque值為YES,減少復(fù)雜圖層合成(如果opaque設(shè)置NO,那么Alpha應(yīng)該小于1)
  • 盡量使用不包含透明(alpha)通道的圖片資源
  • 盡量設(shè)置layer的大小值為整形值
  • 直接讓美工把圖片切成圓角進(jìn)行顯示,這是效率最高的一種方案
  • 很多情況下用戶上傳圖片進(jìn)行顯示,可以讓服務(wù)端處理圓角
  • 使用代碼手動生成圓角Image設(shè)置到要顯示的View上,利用UIBezierPath(CoreGraphics框架)畫出來圓角圖片

3、其它

  • 1) 減少視圖的數(shù)目:
    我們在cell上添加系統(tǒng)控件的時(shí)候,實(shí)際上系統(tǒng)都會調(diào)用底層的接口進(jìn)行繪制,大量添加控件時(shí),會消耗很大的資源并且也會影響渲染的性能。當(dāng)使用默認(rèn)的UITableViewCell并且在它的ContentView上面添加控件時(shí)會相當(dāng)消耗性能。所以目前最佳的方法還是繼承UITableViewCell,并重寫drawRect方法,并且這里的繪制過程可以通過多線程異步繪制。
  • 2)減少多余的繪制操作:
    在實(shí)現(xiàn)drawRect方法的時(shí)候,它的參數(shù)rect就是我們需要繪制的區(qū)域,在rect范圍之外的區(qū)域我們不需要進(jìn)行繪制,否則會消耗相當(dāng)大的資源。
  • 3)不要給cell動態(tài)添加subView:
    在初始化cell的時(shí)候就將所有需要展示的添加完畢,然后根據(jù)需要來設(shè)置hide屬性顯示和隱藏。
  • 4)滑動時(shí)按需加載對應(yīng)的內(nèi)容:
    滑動很快時(shí),只加載目標(biāo)范圍內(nèi)的cell,這樣按需加載(配合SDWebImage),極大提高流暢度,但是這樣在滑動過程中就會暫時(shí)顯示空白,這就需要在性能和用戶體驗(yàn)中權(quán)衡了。

最后安利一篇YY大神的博客
YY大神:iOS 保持界面流暢的技巧.

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

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

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