iOS-UITableView性能優化

1.最常用的就是cell的重用, 注冊重用標識符
它的原理是,根據cell高度和tableView大小,確定界面上能顯示幾個cell,例如界面上只能顯示6個cell,那么這6個cell都是單獨創建的而不是根據重用標識符去緩存中找到的。當你開始滑動tableView時,第一個cell開始漸漸消失,第七個cell開始顯示的時候,會創建第七個cell,而不是用第一個cell去顯示在第七個cell位置,因為有可能第一個cell顯示了一半,而第7個cell也顯示了一半,這個時候第一個cell還沒有被放入緩存中,緩存中沒有可利用的cell。所以實際上創建了7個cell。當滑動tableView去顯示第八個cell的時候,這時緩存中已經有第一個cell,那么系統會直接從緩存中拿出來而不是創建,這樣就算有100個cell的數據需要顯示,實際也只消耗7個cell的內存。

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

  • 盡量少在cellForRowAtIndexPath中設置數據,假如有100個數據,那么cellForRowAtIndexPath會執行100次,但實際屏幕顯示卻只有幾個。這樣會大量消耗時間,可以在willDisplayCell里進行數據的設置,因為willDisplayCell只會在cell將要顯示時調用,屏幕顯示幾個cell才會調用。可以大大減少數據設置時間

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
//不要去設置cell的數據
}
-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
//當cell要顯示時才去設置需要顯示的cell對應的數據
}


2.避免cell的重新布局

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

  • 各個信息都是根據之前算好的布局進行繪制的。需要異步繪制。重寫drawRect方法就不需要異步繪制了,因為drawRect本來就是異步繪制的。圖文混排的繪制,coreText繪制。

3.提前計算并緩存cell的屬性及內容

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

4.使用局部更新

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

5.減少cell中控件的數量

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

6.緩存行高

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

7.使用不透明視圖

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

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

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

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

R = S + D * ( 1 – Sa )

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

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

R = S

即不管D為什么,結果都一樣。因此GPU將不會做任何的計算合成,不需要考慮它下方的任何東西(因為都被它遮擋住了),而是簡單從這個層拷貝。這節省了GPU相當大的工作量。由此看來,opaque屬性的真實用處是給繪圖系統提供一個性能優化開關!

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

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.

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


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

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

8.cell動畫和繪制

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

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

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

  • 當圖片下載完成后,如果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:方法,插入新行需要在主線程執行,而一次插入很多行的話(例如50行),會長時間阻塞主線程。而換成reloadData方法的話,瞬間就處理完了。

9.減少視圖的數目

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

10.不要阻塞主線程

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

- (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內部圖片處理
UIImage類方法總結

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

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

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

  • 圓角圖片處理
  • 1.直接在原圖上層覆蓋一個內部透明圓的圖片。(目前來說最優的方式)
  • 2.重新繪制圖片(雖然重新繪制后會減少渲染的計算,但還是會影響渲染。這種方式只是把GPU的壓力轉義到了CPU上。負載平衡)。下面是繪制圖片的方法
//根據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);
}

當然這里圓角的處理最好還是使用不透明的mask來遮罩。既能不用因為繪制造成CPU計算,而多余區域的渲染造成GPU的計算。

Tip:復制下YY大神說的話和Demo

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

滾動時調整視圖的繪制行為

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

參考資料:UITableView性能優化與卡頓問題

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,321評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,559評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,442評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,835評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,581評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,922評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,931評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,096評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,639評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,374評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,591評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,104評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,789評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,196評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,524評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,322評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,554評論 2 379

推薦閱讀更多精彩內容