iOS-UITableView性能優(yōu)化

1.最常用的就是cell的重用, 注冊重用標(biāo)識符
它的原理是,根據(jù)cell高度和tableView大小,確定界面上能顯示幾個cell,例如界面上只能顯示6個cell,那么這6個cell都是單獨(dú)創(chuàng)建的而不是根據(jù)重用標(biāo)識符去緩存中找到的。當(dāng)你開始滑動tableView時,第一個cell開始漸漸消失,第七個cell開始顯示的時候,會創(chuàng)建第七個cell,而不是用第一個cell去顯示在第七個cell位置,因?yàn)橛锌赡艿谝粋€cell顯示了一半,而第7個cell也顯示了一半,這個時候第一個cell還沒有被放入緩存中,緩存中沒有可利用的cell。所以實(shí)際上創(chuàng)建了7個cell。當(dāng)滑動tableView去顯示第八個cell的時候,這時緩存中已經(jīng)有第一個cell,那么系統(tǒng)會直接從緩存中拿出來而不是創(chuàng)建,這樣就算有100個cell的數(shù)據(jù)需要顯示,實(shí)際也只消耗7個cell的內(nèi)存。

  • 重用
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    NSString *identifier = @"CellIdentifier";//標(biāo)識符
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];//沖緩沖池中取出同樣標(biāo)識的cell
    if(cell == nil)
    {
        //緩沖池中沒有時,創(chuàng)建新的
        cell = [[UITableViewCell alloc]init........];
    }
return cell;
}
  • 如果cell內(nèi)部顯示的內(nèi)容來自web,使用異步加載,緩存結(jié)果請求。

  • 盡量少在cellForRowAtIndexPath中設(shè)置數(shù)據(jù),假如有100個數(shù)據(jù),那么cellForRowAtIndexPath會執(zhí)行100次,但實(shí)際屏幕顯示卻只有幾個。這樣會大量消耗時間,可以在willDisplayCell里進(jìn)行數(shù)據(jù)的設(shè)置,因?yàn)閣illDisplayCell只會在cell將要顯示時調(diào)用,屏幕顯示幾個cell才會調(diào)用。可以大大減少數(shù)據(jù)設(shè)置時間

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
//不要去設(shè)置cell的數(shù)據(jù)
}
-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
//當(dāng)cell要顯示時才去設(shè)置需要顯示的cell對應(yīng)的數(shù)據(jù)
}


2.避免cell的重新布局

  • 創(chuàng)建cell的時候就完成布局,在后期設(shè)置cell屬性的時候盡量少去添加移除cell內(nèi)部控件的布局,盡量用hidden控制,在那種界面變動較大的界面或者控件較多的界面,盡量用多個 注冊重用標(biāo)識符或者不用cell來代表,這樣減少內(nèi)部重新布局帶來的計算,雖然多個重用標(biāo)識符會帶來內(nèi)存變多,但相比讓用戶感覺界面流暢,這點(diǎn)犧牲是有必要的。

  • 各個信息都是根據(jù)之前算好的布局進(jìn)行繪制的。需要異步繪制。重寫drawRect方法就不需要異步繪制了,因?yàn)閐rawRect本來就是異步繪制的。圖文混排的繪制,coreText繪制。

3.提前計算并緩存cell的屬性及內(nèi)容

  • 當(dāng)我們創(chuàng)建cell的數(shù)據(jù)源方法時,編譯器并不是先創(chuàng)建cell 再定cell的高度
  • 而是先根據(jù)內(nèi)容一次確定每一個cell的高度,高度確定后,再創(chuàng)建要顯示的cell,滾動時,每當(dāng)cell進(jìn)入憑虛都會計算高度,提前估算高度告訴編譯器,編譯器知道高度后,緊接著就會創(chuàng)建cell,這時再調(diào)用高度的具體計算方法,這樣可以防止浪費(fèi)時間去計算顯示以外的cell
  • cell內(nèi)部盡量少計算。比如文字的寬度,圖片的寬高等,盡量在model設(shè)置前就計算好cell的高度。而不要在cell內(nèi)部去進(jìn)行計算,阻塞線程.(加入cell高度計算比較復(fù)雜,可以設(shè)置一個類似與cell內(nèi)部計算的view,在創(chuàng)建model數(shù)據(jù)的時候,用這個view預(yù)先計算出cell的高度,而不是在cell內(nèi)部或者tableView:heightForRowAtIndexPath方法里去計算,設(shè)置cell數(shù)據(jù)的時候高度直接從model里拿出。view的計算可以用異步線程去計算,但是不能讓用戶等待cell刷新時間過長)

4.使用局部更新

  • 如果只是更新某組的話,使用reloadSection進(jìn)行局部更新
  • 如果目標(biāo)行與當(dāng)前行相差超過指定行數(shù),只在目標(biāo)滾動范圍的前后制定n行加載。滾動很快時,只加載目標(biāo)范圍內(nèi)得cell,這樣按需加載,極大地提高了流暢性

5.減少cell中控件的數(shù)量

  • 盡量使cell得布局大致相同,不同風(fēng)格的cell可以使用不用的重用標(biāo)識符,初始化時添加控件(見仁見智哈,看個人對界面的分析領(lǐng)會)
  • 不適用的可以先隱藏

6.緩存行高

estimatedHeightForRow不能和HeightForRow里面的layoutIfNeed同時存在,這兩者同時存在才會出現(xiàn)“竄動”的bug。所以我的建議是:只要是固定行高就寫預(yù)估行高來減少行高調(diào)用次數(shù)提升性能。如果是動態(tài)行高就不要寫預(yù)估方法了,用一個行高的緩存字典來減少代碼的調(diào)用次數(shù)即可

7.使用不透明視圖

  • 不透明的視圖可以極大地提高渲染的速度。因此如非必要,可以將table cell及其子視圖的opaque屬性設(shè)為YES(默認(rèn)值UIButton內(nèi)部的label的opaque默認(rèn)值都是NO])。
  • Cell中不要使用clearColor,無背景色,透明度也不要設(shè)置為0。
  • 關(guān)于opaque

Opaque該屬性為BOOL值,UIView的默認(rèn)值是YES,但UIButton等子類的默認(rèn)值都是NO。opaque表示當(dāng)前UIView是否不透明,不過搞笑的是事實(shí)上它卻決定不了當(dāng)前UIView是不是不透明,比如你將opaque設(shè)為NO,該UIView照樣是可見的(是否可見是由alpha或hidden屬性決定的),照理說為NO就表示透明,那就應(yīng)該是不可見的呀?

顯示器中的每個像素點(diǎn)都可以顯示一個由RGBA顏色空間組成的色值,比如有紅色和綠色兩個圖層色塊,對于沒有交叉的部分,即純紅色和綠色部分來說,對應(yīng)位置的像素點(diǎn)只需要簡單的顯示紅或綠,對應(yīng)的RGBA為(1,0,0,1)和(0,1,0,1)就行了,負(fù)責(zé)圖形顯示的GPU需要很小的計算量就可以確定像素點(diǎn)對應(yīng)的顯示內(nèi)容。

問題是紅色和綠色還有相交的一塊,其相交的顏色為黃色。這里的黃色是怎么來的呢?原來,GPU會通過圖層一和圖層二的顏色進(jìn)行圖層混合,計算出混合部分的顏色,最理想情況的計算公式如下:

R = S + D * ( 1 – Sa )

其中,R表示混合結(jié)果的顏色,S是源顏色(位于上層的紅色圖層一),D是目標(biāo)顏色(位于下層的綠色圖層二),Sa是源顏色的alpha值,即透明度。公式中所有的S和D顏色都假定已經(jīng)預(yù)先乘以了他們的透明度。

知道圖層混合的基本原理以后,再回到正題說說opaque屬性的作用。當(dāng)UIView的opaque屬性被設(shè)為YES以后,按照上面的公式,也就是Sa的值為1,這個時候公式就變成了:

R = S

即不管D為什么,結(jié)果都一樣。因此GPU將不會做任何的計算合成,不需要考慮它下方的任何東西(因?yàn)槎急凰趽踝×?,而是簡單從這個層拷貝。這節(jié)省了GPU相當(dāng)大的工作量。由此看來,opaque屬性的真實(shí)用處是給繪圖系統(tǒng)提供一個性能優(yōu)化開關(guān)!

按照前面的邏輯,當(dāng)opaque屬性被設(shè)為YES時,GPU就不會再利用圖層顏色合成公式去合成真正的色值。因此,如果opaque被設(shè)置成YES,而對應(yīng)UIView的alpha屬性不為1.0的時候,就會有不可預(yù)料的情況發(fā)生,這一點(diǎn)蘋果在官方文檔中有明確的說明:

An opaque view is expected to fill its bounds with entirely opaque content—that is, the content should have an alpha value of 1.0. If the view is opaque and either does not fill its bounds or contains wholly or partially transparent content,the results are unpredictable. You should always set the value of this property to NO if the view is fully or partially transparent.

  • 舉例
    比如,如果當(dāng)前我們擁有一個和屏幕大小一致的單一圖層,那么屏幕上的每一個像素相當(dāng)于圖層中的一個像素,這個時候,我們在這個圖層上放置一個完全不透明的圖層,那么GPU將會把上面的圖層合成到下面的圖層當(dāng)中,由于上面的是一個完全不透明的圖層,所以上面的圖層會部份遮蓋掉下面的圖層,而在遮蓋掉的矩形區(qū)域內(nèi),GPU會直接使用上面圖層的像素來顯示。如果我們最底的圖層上放置的是一個有透明度的圖層,那么在這個矩形區(qū)域里,GPU需要混合上下兩個圖層來計算出在屏幕上顯示出來的像素的RGB值。若在同一個區(qū)域內(nèi),存在著多個有透明度的圖層,那么GPU需要更多的計算才能得出最終像素的RGB值。而我們要做的就是避免像素混合,盡可能地為視圖設(shè)置背景色,且設(shè)置opaque為YES,這會大大減少GPU的計算。
    這種顏色的混合需要消耗一定的GPU,在實(shí)際開發(fā)中遠(yuǎn)不止2層。如果只顯示最上層,建議最上次透明度為1和opaque為YES.這樣GPU就不會計算其他層的layer,減少計算。
//以下這種處理方式會出現(xiàn)UILabel出現(xiàn)未知的邊框,解決辦法有2種
1.讓uilbale的寬高都為正數(shù)
2.設(shè)置UILabel的邊框顏色為自己的背景顏色。
這2種辦法雖然可以解決,但是在按鈕中時,點(diǎn)擊按鈕會出現(xiàn)邊框


//IOS8以后UILabel的底圖層變成了_UILabelLayer
//如果label的內(nèi)容是中文,label實(shí)際渲染區(qū)域要大于label的size
//所以只要UILabel中含有中文,比如會造成像素混合增加GPU的計算。
//cell中的UILabel和button里的label沒有設(shè)置background,都是默認(rèn)的。
//要不造成像素混合,需要讓UILabel有背景,并設(shè)置masksToBounds來排除像素混合

self.label.background = self.contentView.background;
self.label.layer.masksToBounds = YES;
  • 注意 :maskTobounds與cornerRadius結(jié)合才會離屏渲染,單獨(dú)使用不會造成離屏渲染

8.cell動畫和繪制

重用時,它內(nèi)部繪制的內(nèi)容并不會被自動清除,因此你可能需要調(diào)用setNeedsDisplayInRect:或setNeedsDisplay方法。

CPU與GPU的說明
CPU就是做繪制的操作把內(nèi)容放到緩存里,GPU負(fù)責(zé)從緩存里讀取數(shù)據(jù)然后渲染到屏幕上。CPU將準(zhǔn)備好的bitmap放到RAM里,GPU去搬這快內(nèi)存到VRAM中處理。 而這個過程GPU所能承受的極限大概在16.7ms完成一幀的處理,所以最開始提到的60fps其實(shí)就是GPU能處理的最高頻率。 GPU是圖形硬件,主要的工作是混合紋理并算出像素的RGB值,這是一個非常復(fù)雜的計算過程,計算的過程越復(fù)雜,所需要消耗的時間就越長,GPU的使用率就越高,這并不是一個好的現(xiàn)像,而我們需要做的是減少GPU的計算量。

如果不需要動畫效果,最好不要使用insertRowsAtIndexPaths:withRowAnimation:方法,而是直接調(diào) 用reloadData方法
利用預(yù)渲染加速iOS設(shè)備的圖像顯示

  • 當(dāng)圖片下載完成后,如果cell是可見的,還需要更新圖像
NSArray *indexPaths = [self.tableView indexPathsForVisibleRows];
for (NSIndexPath *visibleIndexPath in indexPaths) {
if (indexPath == visibleIndexPath) { 
MyTableViewCell *cell = (MyTableViewCell *)[self.tableView cellForRowAtIndexPath:indexPath];
cell.image = image; 
[cell setNeedsDisplayInRect:imageRect]; break; 
}
}// 也可不遍歷,直接與頭尾相比較,看是否在中間即可。

insertRowsAtIndexPaths:withRowAnimation:方法,插入新行需要在主線程執(zhí)行,而一次插入很多行的話(例如50行),會長時間阻塞主線程。而換成reloadData方法的話,瞬間就處理完了。

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

textLabel、detailTextLabel和imageView等view,而你還可以自定義一些視圖放在它的contentView里。然而view是很大的對象,創(chuàng)建它會消耗較多資源,并且也影響渲染的性能。如果你的table cell包含圖片,且數(shù)目較多,使用默認(rèn)的UITableViewCell會非常影響性能。奇怪的是,使用自定義的view,而非預(yù)定義的view,明顯會快些。

10.不要阻塞主線程

出現(xiàn)這種現(xiàn)象的原因就是主線程執(zhí)行了耗時很長的函數(shù)或方法,在其執(zhí)行完畢前,無法繪制屏幕和響應(yīng)用戶請求。其中最常見的就是網(wǎng)絡(luò)請求了,它通常都需要花費(fèi)數(shù)秒的時間,而你不應(yīng)該讓用戶等待那么久。
解決辦法就是使用多線程,讓子線程去執(zhí)行這些函數(shù)或方法。這里面還有一個學(xué)問,當(dāng)下載線程數(shù)超過2時,會顯著影響主線程的性能。因此在使用ASIHTTPRequest時,可以用一個NSOperationQueue來維護(hù)下載請求,并將其maxConcurrentOperationCount設(shè)為2。而NSURLRequest則可以配合GCD來實(shí)現(xiàn),或者使用NSURLConnection的setDelegateQueue:方法。
當(dāng)然,在不需要響應(yīng)用戶請求時,也可以增加下載線程數(shù),以加快下載速度:

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
    if (!decelerate) {
        queue.maxConcurrentOperationCount = 5;
    }
}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    queue.maxConcurrentOperationCount = 5;
}

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
    queue.maxConcurrentOperationCount = 2;
}

11.cell內(nèi)部圖片處理
UIImage類方法總結(jié)

假如內(nèi)存里有一張400x400的圖片,要放到100x100的imageview里,如果不做任何處理,直接丟進(jìn)去,問題就大了,這意味著,GPU需要對大圖進(jìn)行縮放到小的區(qū)域顯示,需要做像素點(diǎn)的sampling,這種smapling的代價很高,又需要兼顧pixel alignment。計算量會飆升。
OpenGL ES是直接調(diào)用底層的GPU進(jìn)行渲染;Core Graphics是一個基于CPU的繪制引擎;

    //重新繪制圖片
    //按照imageWidth, imageHeight指定寬高開始繪制圖片
    UIGraphicsBeginImageContext(CGSizeMake(imageWidth, imageHeight));
    //把image原圖繪制成指定寬高
    [image drawInRect:CGRectMake(0,0,imageWidth,  imageHeight)];
    //從繪制中獲取指定寬高的圖片
    UIImage* newImage = UIGraphicsGetImageFromCurrentImageContext();
    //結(jié)束繪制
    UIGraphicsEndImageContext();

RunLoop開始,RunLoop是一個60fps的回調(diào),也就是說每16.7ms繪制一次屏幕,也就是我們需要在這個時間內(nèi)完成view的緩沖區(qū)創(chuàng)建,view內(nèi)容的繪制這些是CPU的工作;然后把緩沖區(qū)交給GPU渲染,這里包括了多個View的拼接(Compositing),紋理的渲染(Texture)等等,最后Display到屏幕上。但是如果你在16.7ms內(nèi)做的事情太多,導(dǎo)致CPU,GPU無法在指定時間內(nèi)完成指定的工作,那么就會出現(xiàn)卡頓現(xiàn)象,也就是丟幀。

  • 圓角圖片處理
  • 1.直接在原圖上層覆蓋一個內(nèi)部透明圓的圖片。(目前來說最優(yōu)的方式)
  • 2.重新繪制圖片(雖然重新繪制后會減少渲染的計算,但還是會影響渲染。這種方式只是把GPU的壓力轉(zhuǎn)義到了CPU上。負(fù)載平衡)。下面是繪制圖片的方法
//根據(jù)size 和 radius 把image重新繪制。
-(UIImage *)getCornerRadius:(UIImage *)image size:(CGSize)size radius:(int)r
{
    int w = size.width;
    int h = size.height;
    int radius = r;

    UIImage *img = image;
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGContextRef context = CGBitmapContextCreate(NULL, w, h, 8, 4 * w, colorSpace, kCGImageAlphaPremultipliedFirst);
    CGRect rect = CGRectMake(0, 0, w, h);

    CGContextBeginPath(context);
    addRoundedRectToPath(context, rect, radius, radius);
    CGContextClosePath(context);
    CGContextClip(context);
    CGContextDrawImage(context, CGRectMake(0, 0, w, h), img.CGImage);
    CGImageRef imageMasked = CGBitmapContextCreateImage(context);
    img = [UIImage imageWithCGImage:imageMasked];

    CGContextRelease(context);
    CGColorSpaceRelease(colorSpace);
    CGImageRelease(imageMasked);
    return img;
}


static void addRoundedRectToPath(CGContextRef context, CGRect rect, float ovalWidth,
                                 float ovalHeight)
{
    float fw, fh;
    
    if (ovalWidth == 0 || ovalHeight == 0)
    {
        CGContextAddRect(context, rect);
        return;
    }
    
    CGContextSaveGState(context);
    CGContextTranslateCTM(context, CGRectGetMinX(rect), CGRectGetMinY(rect));
    CGContextScaleCTM(context, ovalWidth, ovalHeight);
    fw = CGRectGetWidth(rect) / ovalWidth;
    fh = CGRectGetHeight(rect) / ovalHeight;
    
    CGContextMoveToPoint(context, fw, fh/2);  // Start at lower right corner
    CGContextAddArcToPoint(context, fw, fh, fw/2, fh, 1);  // Top right corner
    CGContextAddArcToPoint(context, 0, fh, 0, fh/2, 1); // Top left corner
    CGContextAddArcToPoint(context, 0, 0, fw/2, 0, 1); // Lower left corner
    CGContextAddArcToPoint(context, fw, 0, fw, fh/2, 1); // Back to lower right
    
    CGContextClosePath(context);
    CGContextRestoreGState(context);
}

當(dāng)然這里圓角的處理最好還是使用不透明的mask來遮罩。既能不用因?yàn)槔L制造成CPU計算,而多余區(qū)域的渲染造成GPU的計算。

Tip:復(fù)制下YY大神說的話和Demo

目前有些第三方微博客戶端(比如 VVebo、墨客等),使用了一種方式來避免高速滑動時 Cell 的繪制過程,相關(guān)實(shí)現(xiàn)見這個項(xiàng)目:VVeboTableViewDemo。它的原理是,當(dāng)滑動時,松開手指后,立刻計算出滑動停止時 Cell 的位置,并預(yù)先繪制那個位置附近的幾個 Cell,而忽略當(dāng)前滑動中的 Cell。這個方法比較有技巧性,并且對于滑動性能來說提升也很大,唯一的缺點(diǎn)就是快速滑動中會出現(xiàn)大量空白內(nèi)容。如果你不想實(shí)現(xiàn)比較麻煩的異步繪制但又想保證滑動的流暢性,這個技巧是個不錯的選擇。

滾動時調(diào)整視圖的繪制行為

滾動會導(dǎo)致數(shù)個視圖在短時間內(nèi)更新,如果視圖的繪制代碼沒有被適當(dāng)調(diào)整,滾動時的性能會非常低,造成卡頓。相對于去考慮如何讓cell視圖內(nèi)部布局簡單控件數(shù)量少,應(yīng)該更加傾向于滾動開始時改變cell視圖顯示方式。例如當(dāng)滑動時暫時性的減少需要顯示的內(nèi)容,或者滾動時改變cell視圖顯示的方式,比如圖片、視頻僅顯示占位圖等。當(dāng)滾動停止時,在將cell視圖顯示狀態(tài)返回到前一狀態(tài)。

參考資料:UITableView性能優(yōu)化與卡頓問題

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

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

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