UITableView性能優(yōu)化以及常見問題匯總

tableView卡頓的原因,從硬件上來說無非就兩個(gè),一個(gè)是CPU原因,一個(gè)是GPU原因.如果CPU核數(shù)較多,并發(fā)處理問題的能力也就越強(qiáng),處理大量計(jì)算也不在話下;如果GPU顯存夠大,渲染能力足夠強(qiáng),處理復(fù)雜圖形界面也就得心應(yīng)手。但是,硬件的配置是有限度的,我們的目標(biāo)是在有限度的硬件上,讓其發(fā)揮最大限度的作用。
1.老生常談cell重用時(shí)引發(fā)的問題??
UITableView中的cell可以有很多,一般會(huì)通過重用cell來達(dá)到節(jié)省內(nèi)存的目的:通過為每個(gè)cell指定一個(gè)重用標(biāo)識(shí)符(reuseIdentifier),即指定了單元格的種類,當(dāng)cell滾出屏幕時(shí),會(huì)將滾出屏幕的單元格放入重用的queue中,當(dāng)某個(gè)未在屏幕上的單元格要顯示的時(shí)候,就從這個(gè)queue中取出單元格進(jìn)行重用。
但對(duì)于多變的自定義cell,有時(shí)這種重用機(jī)制會(huì)出錯(cuò)。比如,當(dāng)一個(gè)cell分割線UI無法系統(tǒng)原生無法滿足采用自定義方案時(shí),這時(shí)如果還有同類cell不需要展示或者UI要求不同的分割線時(shí)就會(huì)出錯(cuò)。3種老方案網(wǎng)上也有很多,還有一種用使用prepareForReuse也不錯(cuò)。

方案一:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 
    //以indexPath來唯一確定cell
    NSString *CellIdentifier = [NSString stringWithFormat:@"Identifier%d%d", [indexPath section], [indexPath row]]; 
    //出列可重用的cell
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];  
    if (cell == nil) { 
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]; 
    } 
    //xxxxxxx 
} 

通過為每個(gè)cell指定不同的重用標(biāo)識(shí)符(reuseIdentifier)來解決。重用機(jī)制是根據(jù)相同的標(biāo)識(shí)符來重用cell的,標(biāo)識(shí)符不同的cell不能彼此重用。于是我們將每個(gè)cell的標(biāo)識(shí)符都設(shè)置為不同,就可以避免不同cell重用的問題了。
方案二:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 
    static NSString *CellIdentifier = @"Cell"; 
    //根據(jù)indexPath準(zhǔn)確地取出一行,而不是從cell重用隊(duì)列中取出 
    UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath]; 
    if (cell == nil) { 
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]; 
    } 
     //...其他代碼                               
 } 

.
重用機(jī)制調(diào)用的就是dequeueReusableCellWithIdentifier這個(gè)方法,方法的意思就是“出列可重用的cell”,因而只要將它換為cellForRowAtIndexPath(只從要更新的cell的那一行取出cell),就可以不使用重用機(jī)制,因而問題就可以得到解決,但是這個(gè)方法會(huì)有很多的cell創(chuàng)建,浪費(fèi)了一些空間。
方案三:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 
    static NSString *CellIdentifier = @"Cell"; 
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) { 
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]; 
    } else { 
        //刪除cell的所有子視圖 
        while ([cell.contentView.subviews lastObject] != nil) { 
            [(UIView*)[cell.contentView.subviews lastObject] removeFromSuperview]; 
        } 
    } 
    //...其他代碼 
}

.
刪除重用cell的所有子視圖;這個(gè)方法是通過刪除重用的cell的所有子視圖,從而得到一個(gè)沒有特殊格式的cell,供其他cell重用。考慮到內(nèi)存問題,cell少得時(shí)候可以每個(gè)都添加標(biāo)識(shí)符,當(dāng)cell重用較多時(shí),考慮內(nèi)存問題,建議用刪除cell的所有子視圖方法。
方案四:
當(dāng)前已經(jīng)被分配的cell如果被重用了(通常是滾動(dòng)出屏幕外了),會(huì)調(diào)用cell的prepareForReuse通知cell。注意這里重寫方法的時(shí)候,注意一定要調(diào)用父類方法[super prepareForReuse] 。

- (void)prepareForReuse {
   [super prepareForReuse];
   //...其他代碼 
   }

2.cell高度問題
這里涉及的主要是減輕CPU負(fù)荷的問題。CPU的主要負(fù)責(zé)快速調(diào)度任務(wù),大量計(jì)算工作,所以在tableView快速滾動(dòng)的過程中讓CPU的計(jì)算量降低是優(yōu)化應(yīng)該考慮的方向。
這個(gè)問題有兩種情況:定高的cell和動(dòng)態(tài)高度的cell。(這里不討論storyboard,xib的情況,通過Interface知道xib或者storyboard本身就是一個(gè)xml文件,添加刪除控件必然中間多了一個(gè)encode/decode過程,增加了cpu的計(jì)算量。并且還要避免臃腫的 XIB 文件,因?yàn)閄IB文件在主線程中進(jìn)行加載布局。當(dāng)用到一些自定義View或者XIB文件時(shí),XIB的加載會(huì)把所有內(nèi)容加載進(jìn)來,如果XIB里面的一些控件并不會(huì)用到,這就可能造成一些資源的消耗浪費(fèi)。storyboard好一些,但也不推薦使用。)
(1)定高的cell
這沒什么好說的,在tableView初始化方法里直接寫死cell高度,例如:

   self.tableView.rowHeight = 120;

.
這個(gè)方法指定tableView涉及范圍內(nèi)所有的cell高度都是120,rowHeight默認(rèn)的值是44。對(duì)于定高cell,直接采用上面方式給定高度,不需要實(shí)現(xiàn)tableView:heightForRowAtIndexPath:方法,這樣可以節(jié)省不必要的計(jì)算和開銷。
(2)動(dòng)態(tài)高度的cell
這里面又有兩種處理方案
<1>提前計(jì)算出cell高度,存在對(duì)應(yīng)的model或者其他相應(yīng)模塊中,實(shí)現(xiàn)代理,來給出高度:

   -(CGFloat)tableView:(UITableView*)tableViewheightForRowAtIndexPath:(NSIndexPath*)indexPath {                
   // return height;        
   }       

.
這個(gè)代理方法實(shí)現(xiàn)后,上面的rowHeight的設(shè)置將會(huì)變成無效。在這個(gè)方法中,我們需要提高cell高度的計(jì)算效率,提前計(jì)算并緩存cell的高度,來節(jié)省時(shí)間。這種方案對(duì)比自動(dòng)布局肯定對(duì)比CPU的損耗肯定是更小的,自動(dòng)布局就是給控件添加約束,約束最終還是轉(zhuǎn)換成frame。在滿足業(yè)務(wù)需求情況下,如果圖層層次較為復(fù)雜,要盡量減少自動(dòng)布局約束,轉(zhuǎn)為手動(dòng)計(jì)算布局,大量的約束重疊也會(huì)增加cpu的計(jì)算量,但也不是說自動(dòng)布局就沒有優(yōu)點(diǎn)。
<2>iOS8之后蘋果為UITableView引入SelfSizing Cell,配合constraints的使用省去了動(dòng)態(tài)計(jì)算Cell高度麻煩,而且有時(shí)還存在著計(jì)算不準(zhǔn)確的尷尬。想要利用這個(gè)特性,cell布局是必須使用AutoLayout,無論是使用Interface Builder方式還是使用Coding方式,前提是必須能理解AutoLayout的布局思路和熟練使用Constraints。使用Masonry的代碼會(huì)更簡(jiǎn)單、簡(jiǎn)潔。當(dāng)然tableView的屬性配置也必不可少:

   self.tableView.estimatedRowHeight = 44.0
   self.tableView.rowHeight = UITableViewAutomaticDimension

.
estimatedRowHeight:默認(rèn)值UITableViewAutomaticDimension。設(shè)置一個(gè)非負(fù)的值可以提高TableView的加載效率,提升體驗(yàn)性,而設(shè)置值為零則禁掉了預(yù)估高度的功能。如果要使用SelfSizing Cell是需要設(shè)置estimatedRowHeight屬性的。
rowHeight:默認(rèn)值UITableViewAutomaticDimension。如果是Coding的話,其實(shí)是不需要顯示設(shè)置這個(gè)屬性,只有用Interface Builder創(chuàng)建的Cell需要顯示設(shè)置tableView.rowHeight = UITableViewAutomaticDimension。

3.高級(jí)一點(diǎn)的優(yōu)化:圖片加載,避免快速滑動(dòng)情況下開過多線程??
說到圖片里面的水太深了,圖片的解壓縮(位圖數(shù)據(jù)到二進(jìn)制數(shù)據(jù)轉(zhuǎn)換的過程)是一個(gè)非常耗時(shí)的 CPU 操作,并且它默認(rèn)是在主線程中執(zhí)行的。那么當(dāng)需要加載的圖片比較多時(shí),就會(huì)對(duì)我們應(yīng)用的響應(yīng)性造成嚴(yán)重的影響,尤其是在快速滑動(dòng)的列表上,這個(gè)問題會(huì)表現(xiàn)得更加突出。既然一定要解壓,耗時(shí),不能卡在主線程,那就拿到子線程解壓,把解壓完的圖片返回之后,再次渲染的時(shí)候,捕捉到已經(jīng)解壓了,就不需要在主線程解壓了,直接顯示。這也是所有第三方圖片框架下載的核心。也就是我們關(guān)心性能優(yōu)化。另外多BB的是,圖片的一般加載方式有兩種:imageNamed 和imageWithContentsOfFile;它們的不同在于前者會(huì)對(duì)圖片進(jìn)行緩存,而后者只是簡(jiǎn)單的從文件加載文件。如果你加載的是大圖,并且只會(huì)用到一次,比如歡迎引導(dǎo)圖,那么就沒必要緩存這個(gè)圖片,可以使用[UIImage imageWithContentsOfFile:],用完就釋放了。如果會(huì)多次使用到一張圖時(shí),用[UIImage imageNamed:] 就會(huì)高效很多,因?yàn)檫@種加載圖片方式有一個(gè)緩存機(jī)制。
回到正題:cell中的圖片會(huì)開異步線程加載(比如常用的第三方:SDWebImage,YYWebImage等),線程開過多了會(huì)造成資源浪費(fèi),內(nèi)存開銷過大。cellForRow方法中我們可以做手腳滾動(dòng)過程中不加載圖片(通過tableview的dragging和declearating兩個(gè)狀態(tài)判斷或者使用runloop),可以在scrollview的代理方法中做限制,當(dāng)滾動(dòng)開始減速的時(shí)候才加載顯示在當(dāng)前屏幕上的cell。
cellForRow限制方案:
方案一:

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
        UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
    if (!cell) {
        cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
    }
    
    /*** 裝配cell其他基礎(chǔ)數(shù)據(jù) ***/
    
    /// 處理圖片
    if (iconImage) {// 有緩存
        cell.imageView.image = iconImage;
    } else {
        if (!tableView.dragging && !tableView.decelerating) {
            /*
            loadImageWithIndexPath:這里要做的事:
            1.下載圖片并緩存
            2.切到主線程展示cell圖片 (UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];定位cell)
            */
            [self loadImageWithIndexPath:indexPath];
         }
    }
    
}

.
方案二:
方案一中的語(yǔ)句:

    if (!tableView.dragging && !tableView.decelerating) {
            /*
            loadImageWithIndexPath:這里要做的事:
            1.下載圖片并緩存
            2.切到主線程展示cell圖片 (UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];定位cell)
            */
            [self loadImageWithIndexPath:indexPath];
         }

.
替換為:

    /**
         runloop - 滾動(dòng)時(shí)候 - trackingMode,
         - 默認(rèn)情況 - defaultRunLoopMode
         ==> 滾動(dòng)的時(shí)候,進(jìn)入`trackingMode`,defaultMode下的任務(wù)會(huì)暫停
         停止?jié)L動(dòng)的時(shí)候 - 進(jìn)入`defaultMode` - 繼續(xù)執(zhí)行`trackingMode`下的任務(wù) - 例如這里的loadImage
    */
    [self performSelector:@selector(loadImageWithIndexPath:)
                   withObject:indexPath afterDelay:0.0
                      inModes:@[NSDefaultRunLoopMode]];

.
接下來是必寫的scrollView方法:

//手放開了 - 停止拖動(dòng)
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{

    if(!decelerate){
        //直接停止-無動(dòng)畫
        [self loadVisibleCellImage];
    }
}

//是否有動(dòng)畫效果
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    [self loadVisibleCellImage];
}

.
這里面loadVisibleCellImage方法核心語(yǔ)句為:

    //拿到界面內(nèi)-所有的cell的indexpath
    NSArray *visableCellIndexPaths = self.tableView.indexPathsForVisibleRows;
    for (NSIndexPath *indexPath in visableCellIndexPaths) {
        [self loadImageWithIndexPath:indexPath];
    }

4.Instruments界面的流暢度檢驗(yàn)這個(gè)網(wǎng)上太多了,可以參考這篇:http://www.itdecent.cn/p/5182234b2e1c
這里提一嘴Color Hits Green and Misses Red等檢測(cè)早就移到Xcode里了,路徑如下,看見網(wǎng)上很多云文章還是在Instruments找感覺奇怪。

15535697613915.jpg

5.離屏渲染等問題日后整理...

參考文章:
http://www.itdecent.cn/p/04457377b48d
http://www.itdecent.cn/p/5182234b2e1c

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

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