iOS 離屏渲染探究

爲何要理解離屏渲染

離屏渲染(Offscreen rendering)對iOS開發者來講不是一個陌生的東西,項目中或多或少都會存在離屏渲染,也是面試中常常考察的知識點。通常來講,大多數人都能知道設置圓角、mask、陰影等會觸發離屏渲染,但咱們深刻的探究一下,你們可以很清楚的知道下面幾個問題嗎?ios

  • 離屏渲染是在哪一步發生的嗎?
  • 離屏渲染產生的緣由是什麼呢?
  • 設置圓角必定會觸發離屏渲染嗎?
  • 離屏渲染既然會影響性能咱們爲何還要使用呢?優化方案又有那些?

今天我就帶着這幾個問題探究一下離屏渲染。面試

ios平臺的渲染框架


Core Animation 流水線:


這是在WWDC的Advanced Graphics and Animations for iOS Apps(WWDC14 419)中有這樣一張圖,咱們能夠看到,在Application這一層中主要是CPU在操做,而到了Render Server這一層,CoreAnimation會將具體操做轉換成發送給GPU的draw calls(之前是call OpenGL ES,如今慢慢轉到了Metal),顯然CPU和GPU雙方同處於一個流水線中,協做完成整個渲染工做。咱們也能夠把iOS下的Core Animation能夠理解爲一個複合引擎,主要職責包含:渲染、構建和實現動畫。算法

離屏渲染的定義

  1. OpenGL中,GPU屏幕渲染有如下兩種方式當前屏幕渲染(On-Screen Rendering):正常狀況下,咱們在屏幕上顯示都是GPU讀取幀緩衝區(Frame Buffer)渲染好的的數據,而後顯示在屏幕上。流程如圖:
  2. (Off-Screen Rendering ):若是有時由於一些限制,沒法把渲染結果直接寫入frame buffer,而是先暫存在另外的內存區域,以後再寫入frame buffer,那麼這個過程被稱之爲離屏渲染。也就是GPU須要在當前屏幕緩衝區之外新開闢一個緩衝區進行渲染操做。流程如圖:
在上面的CoreAnimation流水線示意圖中,咱們能夠得知主要的渲染操做是由CoreAnimation的Render Server模塊,經過調用顯卡驅動提供的OpenGL或Metal接口執行, 對於每一層layer,Render Server會遵循「 畫家算法 」(由遠及近),按次序輸出到frame buffer, 而後按照次序繪製到屏幕,當繪製完一層,就會將該層從幀緩存區中移除(以節省空間) 以下圖,從左至右依次輸出, 獲得最後的顯示結果。


但在某些場景下「畫家算法」雖然能夠逐層輸出,可是沒法在某一層渲染完成後,在回過頭來擦除/修改某一部分,由於這一層以前的layer像素數據已經被永久覆蓋了。這就意味着對於每一層的layer要麼可以經過單次遍歷就能完成渲染,要麼就只能令開闢一塊內存做爲臨時中轉區來完成複雜的修改/裁剪等操做。緩存

舉例說明: 對圖3進行圓角和裁剪:imageView.clipsToBounds = YES,imageView.layer.cornerRadius=10時,這就不是簡單的圖層疊加了, 圖1,圖2,圖3渲染完成後, 還要進行裁減,並且 子視圖layer由於父視圖有圓角,也須要被裁剪, 沒法在某一層渲染完成以後,再回過頭來擦除/改變其中的某個部分。因此不能按照正常的流程,所以蘋果會先渲染好每一層,存入一個緩衝區中,即 離屏緩衝區 ,而後通過層疊加和處理後,再存儲到幀緩存去中,而後繪製到屏幕上,這種處理方式叫作 離屏渲染

 常見離屏渲染場景分析

使用Simulator檢測項目中觸發離屏渲染的圖層,以下圖:bash


打開 Color Off-screen Rendered,同時咱們能夠藉助Xcode或 Reveal 清楚的看到那些圖層觸發了離屏渲染。併發

關於常見的設置圓角觸發離屏渲染示例說明:app


如上圖示例代碼中(btn.png是一個200x300的本地圖片),框架

  • btn1設置了圖片,設置了圓角,打開了clipsToBounds = YES,觸發了離屏渲染,
  • btn2設置了背景顏色,設置了圓角,打開了clipsToBounds = YES,沒有觸發離屏渲染,
  • img1設置了圖片,設置了圓角,打開了masksToBounds = YES,觸發了離屏渲染,
  • img2設置了背景顏色,設置了圓角,打開了masksToBounds = YES,沒有觸發離屏渲染
解釋:btn1和img1觸發了離屏渲染,緣由是btn1是由它的layer和UIImageView的layer混合起來的效果(UIButton有imageView),因此設置圓角的時候會觸發離屏渲染。img1設置cornerRadius和masksToBounds是不會觸發離屏渲染的,若是再對img1設置背景色,則會觸發離屏渲染。

根據示例能夠得出只是控件設置了圓角或(圓角+裁剪)並不會觸發離屏渲染,同時須要知足父layer須要裁剪時,子layer也由於父layer設置了圓角也須要被裁剪(即視圖contents有內容併發生了多圖層被裁剪)時纔會觸發離屏渲染。post

蘋果官方文檔對於cornerRadius的描述:性能

Setting the radius to a value greater than 0.0 causes the layer to begin drawing rounded corners on its background. By default, the corner radius does not apply to the image in the layer’s contents property; it applies only to the background color and border of the layer. However, setting the masksToBounds property to true causes the content to be clipped to the rounded corners.

設置cornerRadius大於0時,只爲layer的backgroundColorborder設置圓角;而不會對layer的contents設置圓角,除非同時設置了layer.masksToBoundstrue(對應UIView的clipsToBounds屬性)。

圓角觸發離屏渲染示意圖


一旦咱們 爲contents設置了內容 ,不管是圖片、繪製內容、有圖像信息的子視圖等,再加上圓角+裁剪,就會觸發離屏渲染。

其餘觸發離屏渲染的場景:

  • 採用了光柵化的 layer (layer.shouldRasterize)
  • 使用了 mask 的 layer (layer.mask)
  • 須要進行裁剪的 layer (layer.masksToBounds /view.clipsToBounds)
  • 設置了組透明度爲 YES,而且透明度不爲 1 的layer (layer.allowsGroupOpacity/ layer.opacity)
  • 使用了高斯模糊
  • 添加了投影的 layer (layer.shadow*)
  • 繪製了文字的 layer (UILabel, CATextLayer, Core Text 等)
shouldRasterize 光柵化

shouldRasterize開啓後,會將layer做爲位圖保存下來,下次直接與其餘內容進行混合。這個保存的位置就是OffscreenBuffer中。這樣下次須要再次渲染的時候,就能夠直接拿來使用了。

shouldRasterize使用建議:

  • layer不復用,不必打開shouldRasterize
  • layer不是靜態的,也就是說要頻繁的進行修改,不必使用shouldRasterize
  • 離屏渲染緩存內容有100ms時間限制,超過該時間的內容會被丟棄,進而沒法複用
  • 離屏渲染空間是屏幕像素的2.5倍,若是超過也沒法複用

離屏渲染的優劣

劣勢

離屏渲染增大了系統的負擔,會形象App性能。主要表如今如下幾個方面:

  • 離屏渲染須要額外的存儲空間,渲染空間大小的上限是2.5倍的屏幕像素大小,超過沒法使用離屏渲染
  • 容易掉幀:一旦由於離屏渲染致使最終存入幀緩存區的時候,已經超過了16.67ms,則會出現掉幀的狀況,形成卡頓
優點

雖然離屏渲染會須要多開闢出新的臨時緩存區來存儲中間狀態,可是對於屢次出如今屏幕上的數據,能夠提早渲染好,從而進行復用,這樣CPU/GPU就不用作一些重複的計算。

特殊產品需求,爲實現一些特殊動效果,須要多圖層以及離屏緩存區保存中間狀態,這種狀況下就不得不使用離屏渲染。好比產品須要實現高斯模糊,不管自定義高斯模糊仍是調用系統API都會觸發離屏渲染。

離屏渲染優化方案(關於實現圓角形成的離屏渲染優化)

方案一

self.view.layer.clipsToBounds = YES;self.view.layer.cornerRadius = 4.f;複製代碼

  • clipsToBounds:UIView中的屬性,其值主要決定了在視圖上的子視圖,超出父視圖的部分是否截取,默認爲NO,即不裁剪子視圖超出部分。
  • masksToBounds:CALayer中的屬性,其值主要決定了視圖的圖層上的子圖層,超出父圖層的部分是否須要裁減掉。默認NO。

方案二

若是產品設計圓角+陰影的卡片,可使用切圖實現圓角+陰影,避免觸發離屏渲染

方案三

貝塞爾曲線繪製圓角

- (UIImage *)imageWithCornerRadius:(CGFloat)radius ofSize:(CGSize)size{
    /* 當前UIImage的可見繪製區域 */
    CGRect rect = (CGRect){0.f,0.f,size};
    /* 建立基於位圖的上下文 */
    UIGraphicsBeginImageContextWithOptions(size, NO, UIScreen.mainScreen.scale);
    /* 在當前位圖上下文添加圓角繪製路徑 */
    CGContextAddPath(UIGraphicsGetCurrentContext(), [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:radius].CGPath);
    /* 當前繪製路徑和原繪製路徑相交獲得最終裁剪繪製路徑 */
    CGContextClip(UIGraphicsGetCurrentContext());
    /* 繪製 */
    [self drawInRect:rect];
    /* 取得裁剪後的image */
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    /* 關閉當前位圖上下文 */
    UIGraphicsEndImageContext();
    return image;
}複製代碼

方案四

CAShapeLayer + UIBezierPath 繪製圓角來實現UITableViewCell圓角並繪製邊框顏色(這種方式比直接設置圓角方式好,但也會觸發離屏渲染),代碼以下:

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath{

    CAShapeLayer *maskLayer = [CAShapeLayer layer];
    maskLayer.frame = CGRectMake(0, 0, cell.width, cell.height);

    CAShapeLayer *borderLayer = [CAShapeLayer layer];
    borderLayer.frame = CGRectMake(0, 0, cell.width, cell.height);
    borderLayer.lineWidth = 1.f;
    borderLayer.strokeColor = COLOR_LINE.CGColor;
    borderLayer.fillColor = [UIColor clearColor].CGColor;

    UIBezierPath *bezierPath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, cell.width, cell.height) cornerRadius:kRadiusCard];
    maskLayer.path = bezierPath.CGPath;
    borderLayer.path = bezierPath.CGPath;

    [cell.contentView.layer insertSublayer:borderLayer atIndex:0];
    [cell.layer setMask:maskLayer];
}


複製代碼

YYKit是開發中常常用的三方庫,YYImage對圖片圓角的處理方法是值得推薦的,附上實現源碼:

- (UIImage *)imageByRoundCornerRadius:(CGFloat)radius
                              corners:(UIRectCorner)corners
                          borderWidth:(CGFloat)borderWidth
                          borderColor:(UIColor *)borderColor
                       borderLineJoin:(CGLineJoin)borderLineJoin {
    
    if (corners != UIRectCornerAllCorners) {
        UIRectCorner tmp = 0;
        if (corners & UIRectCornerTopLeft) tmp |= UIRectCornerBottomLeft;
        if (corners & UIRectCornerTopRight) tmp |= UIRectCornerBottomRight;
        if (corners & UIRectCornerBottomLeft) tmp |= UIRectCornerTopLeft;
        if (corners & UIRectCornerBottomRight) tmp |= UIRectCornerTopRight;
        corners = tmp;
    }
    
    UIGraphicsBeginImageContextWithOptions(self.size, NO, self.scale);
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height);
    CGContextScaleCTM(context, 1, -1);
    CGContextTranslateCTM(context, 0, -rect.size.height);
    
    CGFloat minSize = MIN(self.size.width, self.size.height);
    if (borderWidth < minSize / 2) {
        UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(rect, borderWidth, borderWidth) byRoundingCorners:corners cornerRadii:CGSizeMake(radius, borderWidth)];
        [path closePath];
        
        CGContextSaveGState(context);
        [path addClip];
        CGContextDrawImage(context, rect, self.CGImage);
        CGContextRestoreGState(context);
    }
    
    if (borderColor && borderWidth < minSize / 2 && borderWidth > 0) {
        CGFloat strokeInset = (floor(borderWidth * self.scale) + 0.5) / self.scale;
        CGRect strokeRect = CGRectInset(rect, strokeInset, strokeInset);
        CGFloat strokeRadius = radius > self.scale / 2 ? radius - self.scale / 2 : 0;
        UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:strokeRect byRoundingCorners:corners cornerRadii:CGSizeMake(strokeRadius, borderWidth)];
        [path closePath];
        
        path.lineWidth = borderWidth;
        path.lineJoinStyle = borderLineJoin;
        [borderColor setStroke];
        [path stroke];
    }
    
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return image;
}


複製代碼

拓展閱讀:

iOS圓角的離屏渲染,你真的弄明白了嗎:關於圓角觸發離屏渲染更詳細的分析

關於iOS離屏渲染的深刻研究:即刻技術團隊對離屏渲染的解析

相關文章
相關標籤/搜索