iOS中的離屏渲染(Off-Screen Rendering)

ios下的離屏渲染

離屏渲染(OffScreen Rendering) 這個概念對於iOS開發者來講並不陌生,對App的性能優化和麪試中不止一次的遇到,今天咱們再來聊一聊這個問題。ios

原本是想寫在上一篇 iOS下的圖像渲染原理 中的,感受篇幅有點長了,影響閱讀體驗,因此單寫了一篇。面試

什麼是離屏渲染

在討論離屏渲染以前,咱們先來看看正常的渲染邏輯。算法

frame-buffer

這裏省略了其餘的渲染細節。GPU 以60 FPS的幀率將渲染結果存儲到幀緩衝區(Frame Buffer), 屏幕把每一幀圖像以60 Hz的頻率刷新顯示。緩存

那麼離屏渲染的大體流程是是什麼樣的呢?性能優化

off-screen-buffer

若是有時由於面臨一些限制,沒法把渲染結果直接寫入Frame Buffer,而是先暫存在另外的內存區域,以後再寫入Frame Buffer,那麼這個過程被稱之爲離屏渲染多線程

GPU 離屏渲染

經過上一篇iOS下的圖像渲染原理的講解,咱們知道主要的渲染操做都是由 GPU CoreAnimationRender Server模塊,經過調用顯卡驅動所提供的OpenGL/Metal接口來執行的。函數

對於每一層layer,Render Server會遵循畫家算法,按次序輸出到Frame Buffer,後一層覆蓋前一層,就能獲得最終的顯示結果。post

可是有一些狀況下,並無這麼簡單。GPU 的Render Server遵循畫家算法,一層一層的進行輸出,可是在某一層渲染完成後,沒法在回過頭來處理或者改變其中的某個部分,由於在這以前的全部層像素數據,已經在渲染結束後被永久覆蓋/丟棄了。性能

若是咱們想對某一層layer進行疊加/裁剪或者其餘複雜的操做,就不得不新開一塊內存區域,來處理這些些更加複雜的操做。優化

CPU 離屏渲染

咱們看過一些文章有提到過CPU離屏渲染,那麼什麼是CPU離屏渲染呢?

若是咱們在UIView中實現了-drawRect方法,就算它的函數體內部實際沒有代碼,系統也會爲這個view申請一塊內存區域,等待CoreGraphics可能的繪畫操做。

這種狀況下,新開闢了一塊CGContext,渲染數據暫時存儲在了CGContext中,而沒有給到Frame Buffer。根據上面的定義來講,沒有把渲染結果直接給到Frame Buffer的,那就屬於離屏渲染了。

可是,全部 CPU 執行的光柵化操做,好比圖片的解碼等等,都沒法直接繪製到 GPU 的Frame Buffer 中,多須要一塊用來中轉的內存區域。固然,咱們知道 CPU 並不擅長渲染,因此咱們應該儘可能避免使用 CPU 渲染。根據蘋果的說法,這並不是真正意義上的離屏渲染,而且若是咱們重寫了-drawRect方法,使用Xcode檢測,也並不會被標記爲離屏渲染

如何檢測項目中哪些圖層觸發了離屏渲染?

在模擬器中經過設置 Color Off-Screen Rendered 來檢查哪些圖層觸發了離屏渲染。

模擬器offscreen-rendering檢查

觸發了離屏渲染的圖層會被標記爲黃色。

模擬器offscreen-rendering演示

觸發離屏渲染的場景

經過設置cornerRadius與masksToBounds達到圓角裁切效果

當須要裁切圖層的內容content,很顯然這就須要開闢一塊內存來操做了。當只設置cornerRadius時,不須要裁切內容,只須要一個帶圓角的邊框,則不會觸發離屏渲染。

shadow

陰影依賴layer自己的形狀等信息,而且根據畫家算法,陰影須要先畫出來,這樣來講就須要在單獨的內存中先進行依賴的合成計算,再添加到Frame Buffer,形成離屏渲染。不過若是咱們可以預先告訴CoreAnimation(經過shadowPath屬性)陰影的幾何形狀,那麼陰影固然能夠先被獨立渲染出來,不須要依賴layer本體,也就再也不須要離屏渲染了。

group opacity

須要將一組圖層畫完以後,再總體加上alpha,最後和底下其餘layer的像素進行組合。顯然也沒法經過一次遍歷就獲得最終結果。

mask

咱們知道mask是應用在layer和其全部子layer的組合之上的,並且可能帶有透明度,那麼其實和group opacity的原理相似,不得不在離屏渲染中完成。

UIBlurEffect

渲染出毛玻璃效果,須要先畫出原圖層,而後capture原圖層,進行水平模糊(Horizontal Blur)和垂直模糊(Vertical Blur),最後進行合成操做。顯然這須要在離屏緩衝區中完成。

shouldRasterize

shouldRasterize一旦被設置爲YESRender Server就會強制把layer的渲染結果(包括其子layer,以及圓角、陰影、group opacity等等)保存在一塊內存中,這樣一來在下一幀仍然能夠被複用,而不會再次觸發離屏渲染。有幾個須要注意的點

  • shouldRasterize的主旨在於下降性能損失,但老是至少會觸發一次離屏渲染。若是你的layer原本並不複雜,也沒有圓角陰影等等,則沒有必要打開shouldRasterize
  • 若是layer的子結構很是複雜,渲染一次所需時間較長,能夠打開shouldRasterize,把layer繪製到一塊緩存,而後在接下來複用這個結果,這樣就不須要每次都從新繪製整個layer樹了
  • 離屏渲染緩存有空間上限,最多不超過屏幕總像素的2.5倍大小,若是超出了會自動被丟棄,且沒法被複用了
  • 離屏渲染緩存內容有時間限制,一旦緩存超過100ms沒有被使用,會自動被丟棄,且沒法被複用了
  • 若是layer不是靜態的,須要被頻繁修改,好比處在動畫之中,那麼開啓shouldRasterize反而影響效率了

若是你沒法僅僅使用Frame Buffer來畫出最終結果,那就只能另開一塊內存空間來儲存中間結果。

圓角問題

一般狀況下,咱們會使用 cornerRadius 來設置圓角

view.layer.cornerRadius = 50;
複製代碼

咱們看過不少文章都在說單獨使用 cornerRadius 是不會觸發離屏渲染的,先來實現一個很是簡單圓角Button,只設置了backgroundColor,沒有setImage:

UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
    btn.frame = (CGRect){
        .origin.x = 100,
        .origin.y = 280,
        .size.width = 100,
        .size.height = 100,
    };
    btn.backgroundColor = [UIColor blueColor];
    btn.layer.cornerRadius = 50;
    [self.view addSubview:btn2];
複製代碼

模擬器offscreen-rendering演示2

此時的確不會觸發離屏渲染,也達到了圓角的目的。

在實際項目中,通常會使用一張圖片做爲Button或者ImageView的背景,這樣若是隻設置cornerRadius ,是達不到圓角的效果的,還須要設置masksToBounds = YES

imageView.layer.cornerRadius = 50;
    imageView.layer.masksToBounds = YES;
複製代碼

這裏用UIImageView來舉例,咱們看下效果

UIImageView *imageView = [[UIImageView alloc] init];
    imageView.frame = (CGRect){
        .origin.x = 100,
        .origin.y = 400,
        .size.width = 100,
        .size.height = 100,
    };
    imageView.layer.cornerRadius = 50;
    imageView.layer.masksToBounds = YES;
    imageView.image = [UIImage imageNamed:@"btn.png"];
    [self.view addSubview:imageView];
複製代碼

模擬器offscreen-rendering演示3

能夠看到,這裏仍然沒有發生離屏渲染。那麼離屏渲染到底和什麼有關係呢?

仍是上面的UIImageView的案例,咱們嘗試設置一下它的backgroundColor

UIImageView *imageView = [[UIImageView alloc] init];
    imageView.frame = (CGRect){
        .origin.x = 100,
        .origin.y = 400,
        .size.width = 100,
        .size.height = 100,
    };
    imageView.layer.cornerRadius = 50;
    imageView.layer.masksToBounds = YES;
    imageView.backgroundColor = [UIColor blueColor];
    imageView.image = [UIImage imageNamed:@"btn.png"];
    [self.view addSubview:imageView];
複製代碼

模擬器offscreen-rendering演示4

在同時設置了backgroundColorsetImage:以後,這裏觸發了離屏渲染。

總結

關於性能優化,就是平衡 CPU 與 GPU 的負載工做,由於要作的事情就那麼多。當 GPU 忙不過來的時候,咱們能夠利用 CPU 的空閒來渲染而後提交給 GPU 顯示,來提升總體的渲染效率。渲染不是CPU的強項,調用CoreGraphics會消耗其至關一部分計算時間,通常來講 CPU 渲染都在後臺線程完成(這也是AsyncDisplayKit的主要思想),而後再回到主線程上,把渲染結果傳回CoreAnimation。這樣一來,多線程間數據同步會增長必定的複雜度。CPU渲染速度不夠快,所以只適合渲染靜態的元素,如文字、圖片。做爲渲染結果的bitmap數量較大,很容易致使OOM。若是你選擇使用 CPU 來作渲染,那麼就沒有理由再觸發 GPU 的離屏渲染了。

相關文章
相關標籤/搜索