關於 iOS 離屏渲染的分析與處理

1、什麼是離屏渲染

有一個場景:以 TioIM 的最近聊天會話列表爲例:每一個cell上的頭像圖片均須要裁剪,使用以下方式設置圓角算法

圖片是異步加載緩存

imageView.backgroundColor = [UIColor whiteColor];
imageView.layer.cornerRadius = 50;
imageView.layer.masksToBounds = YES;
複製代碼

當滑動列表時,且當數據量比較大時,快速滑動會發現列表卡頓,咱們進行以下操做,查看bash

開啓模擬器的離屏渲染,會發現網絡

imageView變成了黃色,說明出現了 離屏渲染異步

咱們修改一下代碼,去掉裁剪masksToBounds佈局

imageView.backgroundColor = [UIColor whiteColor];
imageView.layer.cornerRadius = 50;
// imageView.layer.masksToBounds = YES;
複製代碼

結果以下:content(image)沒有裁剪,也沒有看到離屏渲染post

咱們再修改一下代碼,去掉backgroundColor

// imageView.backgroundColor = [UIColor whiteColor];
imageView.layer.cornerRadius = 50;
imageView.layer.masksToBounds = YES;
複製代碼

黃色部分消失優化

因此說設置了 cornerRadius 未必會致使離屏渲染,動畫

出現離屏渲染後,渲染顯示發生了什麼變化?

沒有離屏渲染時的步驟ui

發生離屏渲染後的渲染步驟

2、爲何會在幀緩存區以前多了離屏渲染?

渲染管線從Core Animation 到 Display 的工做流程

若是渲染的畫面比較複雜,如上面案例,背景色backgroundColor須要渲染,content(image)也須要裁剪渲染,若是不按離屏渲染 的步驟,根據畫家算法,先顯示遠處的背景(類比平時子控件佈局,最早顯示的在最下面):

  • 背景色backgroundColor渲染以後的位圖先進入 幀緩衝區->屏幕,幀緩衝區的backgroundColor清空
  • content(image)的位圖再進入 幀緩衝區->屏幕 ,幀緩衝區的content(image)被清空。
  • content(image)裁剪????沒有東西可裁剪了,剛剛幀緩衝區已經被清空了。

因此,須要額外開闢一塊緩衝區,等待合成、裁剪完成後-->幀緩衝區-->屏幕顯示。 那麼,這個額外的處理複雜渲染數據的地方就是 離屏渲染緩衝區(Offscreen Buffer)。 因此,在幀緩衝區以前要多了一個離屏渲染緩衝區。

下面是RenderServer進入GPU到顯示前,以流程圖的形式將上面的文字直觀化出來:

  • 第一步:先渲染紋理(Texture),紋理能夠理解成content內容,在imageview裏就是image。
  • 第二步:渲染背景綠色
  • 第三步:在開闢的離屏渲染緩衝區裏,將紋理和背景組合 -> Frame Buffer -> 屏幕顯示

若是沒有cornerRadius,不會出現離屏渲染

  • 背景色backgroundColor渲染以後的位圖先進入 幀緩衝區->屏幕,幀緩衝區的backgroundColor清空
  • content(image)的位圖再進入 幀緩衝區->屏幕 ,幀緩衝區的content(image)被清空。
  • 結束

直接循序漸進,按照畫家算法一層一層顯示到屏幕就OK,根本用不着等到離屏渲染緩衝區進行額外的處理完再顯示。

若是cell有離屏渲染 tableview 在快速滑動時出現卡頓

當咱們知道爲何會觸發離屏渲染,以及離屏渲染會在渲染流程哪一步進行,咱們再來看看tableview的卡頓問題。固然,促使tableview卡頓的緣由有不少,咱們只談由於離屏渲染致使的卡頓。

若是每個cell都會有一個甚至若干個控件會出現離屏渲染,其中每個渲染都要等到離屏渲染結束後纔給幀緩衝區等待顯示,看上圖的渲染管線的流程,GPU在沒有離屏渲染時是16.67ms,若是有離屏渲染處理,在大量cell快速滾動,出現的cell不斷從新渲染,內部的多個離屏渲染也在進行大量的耗時處理,這就太可怕了。因此在這種時候,咱們要優化離屏渲染。

3、何時會觸發離屏渲染/哪些場景下會有離屏渲染?

不少同窗會很是熟悉文章開頭的場景cornerRadius+masksToBounds。

一、cornerRadius+masksToBounds

在前面的會話列表幾個不一樣狀況中,咱們已經能夠看到cornerRadius+masksToBounds並非必定會引起離屏渲染,只有當。關鍵是要看有幾層渲染數據:backgroundcolor、content(image、text等)。

// img1 content masksToBounds
    UIImageView *img1 = [UIImageView.alloc initWithFrame:CGRectMake(100, 30, 100, 100)];
    img1.image = [UIImage imageNamed:@"btn.png"];
    img1.layer.cornerRadius = 50;
    img1.layer.masksToBounds = YES;
    [self.view addSubview:img1];
    
    // img2 只有backgroundColor
    UIImageView *img2 = [UIImageView.alloc initWithFrame:CGRectMake(100, 180, 100, 100)];
    img2.layer.cornerRadius = 50;
    img2.backgroundColor = UIColor.redColor;
    [self.view addSubview:img2];
    
    // content + backgroundColor
    UIImageView *img3 = [UIImageView.alloc initWithFrame:CGRectMake(100, 320, 100, 100)];
    img3.image = [UIImage imageNamed:@"btn.png"];
    img3.backgroundColor = UIColor.redColor;
    img3.layer.cornerRadius = 50;
    [self.view addSubview:img3];
    
    // content + backgroundColor + masksToBounds
    UIImageView *img4 = [UIImageView.alloc initWithFrame:CGRectMake(100, 480, 100, 100)];
    img4.image = [UIImage imageNamed:@"btn.png"];
    img4.backgroundColor = UIColor.redColor;
    img4.layer.cornerRadius = 50;
    img4.layer.masksToBounds = YES;
    [self.view addSubview:img4];
複製代碼

並且,咱們還能看到另外的一個知識點:cornerRadius並非對content(image)有效,根據蘋果官方解釋:cornerRadius 的文檔中明確說明對 cornerRadius 的設置只對 CALayer 的 backgroundColor 和 borderWidth、borderColor起做用。

cornerRadius+masksToBounds 只有在設置了content且背景不是透明時,纔會出現離屏渲染。

若是必定要使用cornerRadius+masksToBounds的方式裁切圖片,不要設置backgroundColor。

二、毛玻璃模糊效果

渲染的位圖並不能直接給幀緩存區等待顯示,而要通過模糊處理以後才能將最後的渲染數據 -> 幀緩衝區-> 顯示。

三、shadow

開啓陰影效果,首先要shadow是一個什麼樣的存在?他又是如何被渲染繪製?

shadow是一個矩形,是一個背景色,是layer的背景,因此是在layer的下面。shadow是根據layer而來,因此要先知道layer才能直到shadow的大小位置。

若是沒有離屏渲染,和前文同樣,按照畫家算法,必須先將shadow放入幀緩存區,先顯示。可是layer沒有,不可能先渲染出shadow,只能利用離屏渲染緩衝區,等待shadow、layer等渲染併合並完成後,再送入幀緩存區等待顯示。

四、shouldRasterize 光柵化

CALayer的屬性 官方文檔給的是:

意思是若是開啓,會將layer最後的渲染,包括陰影、裁切等的最終效果變成位圖放入離屏緩衝區,等待複用。可是,離屏緩衝區的大小不能超過屏幕的2.5倍,不然被釋放;其次,layer若是不是靜態的,好比imageview的image須要改變,label的text會發生改變等會發生頻繁改變的,開啓shouldRasterize離屏渲染會影響效率;還有,離屏緩衝區是有時間限制的,超過100ms若是沒有被使用,也會被釋放。

因此,咱們要善用shouldRasterize:

  • 若是layer不是靜態的,不建議開啓
  • 若是layer不能不能被複用,也不建議開啓,cell被複用,動畫中的layer等
  • 超過100ms沒有被複用,也不建議開啓
  • 超出離屏渲染控件 屏幕像素的2.5倍,也不建議開啓

五、group opacity 組透明度

有不少sublayer,當咱們對大的layer設置alpha時,會首先在離屏渲染緩衝區等待整個layer裏面的sublayer所有完成後,再根據組透明度opacity計算新的顏色,再和下面的layer顏色整合,纔會給幀緩衝區等待顯示。 因此並非每渲染一層sublayer就立馬給顯示。若是opacity爲1則不須要調整透明度,正常畫家算法顯示。

六、使用了 masks

masklayer 做爲遮罩,顯示在其所在的大layer以及大layer的全部子sublayer之上。masklayer可能也會帶有透明度、形狀(例如,顯示指定區域內的的內容)等。

面對上面的一種狀況,咱們必須在離屏渲染緩衝區內完成Image和Mask的裁切合並處理,才能將最終的Masked Image -> 幀緩衝區顯示。

4、圓角裁切/裁剪異步加載的網絡圖片

我在實際工做當中,遇到特別是針對 異步加載後的網絡圖片進行圓角裁切, 具體寫在個人博客 SDWebImage網絡圖片的圓角裁切和不變形處理 有興趣的同窗能夠看一看。主要解決:

  • UIImageView 異步下載網絡圖後,不拉伸變形顯示
  • 沒有離屏渲染的圓角裁剪
  • 網絡圖片圓角裁剪

部分裁切的核心代碼

- (instancetype)imageWithCornerRadius:(CGFloat)cornerRadius size:(CGSize)newSize
{
    UIImage *originImage = self;
    
    // 開始裁切圓角
    CGRect bounds = CGRectMake(0, 0, newSize.width, newSize.height);
    UIGraphicsBeginImageContextWithOptions(newSize, NO, UIScreen.mainScreen.scale);
    CGContextRef context = UIGraphicsGetCurrentContext();
    UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:bounds
                                                    cornerRadius:cornerRadius];
    CGContextAddPath(context, path.CGPath);
    CGContextClip(context);
    [originImage drawInRect:bounds];
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return image;
}

複製代碼

具體請移步 SDWebImage網絡圖片的圓角裁切和不變形處理

相關文章
相關標籤/搜索