離屏渲染(OffScreen Rendering)
這個概念對於iOS開發者來講並不陌生,對App的性能優化和麪試中不止一次的遇到,今天咱們再來聊一聊這個問題。ios
原本是想寫在上一篇 iOS下的圖像渲染原理 中的,感受篇幅有點長了,影響閱讀體驗,因此單寫了一篇。面試
在討論離屏渲染
以前,咱們先來看看正常的渲染邏輯。算法
這裏省略了其餘的渲染細節。GPU 以60 FPS
的幀率將渲染結果存儲到幀緩衝區(Frame Buffer)
, 屏幕把每一幀圖像以60 Hz
的頻率刷新顯示。緩存
那麼離屏渲染
的大體流程是是什麼樣的呢?性能優化
若是有時由於面臨一些限制,沒法把渲染結果直接寫入Frame Buffer
,而是先暫存在另外的內存區域,以後再寫入Frame Buffer
,那麼這個過程被稱之爲離屏渲染
。markdown
經過上一篇iOS下的圖像渲染原理的講解,咱們知道主要的渲染操做都是由 GPU CoreAnimation
的Render Server
模塊,經過調用顯卡驅動所提供的OpenGL/Metal
接口來執行的。多線程
對於每一層layer,Render Server
會遵循畫家算法
,按次序輸出到Frame Buffer
,後一層覆蓋前一層,就能獲得最終的顯示結果。函數
可是有一些狀況下,並無這麼簡單。GPU 的Render Server
遵循畫家算法
,一層一層的進行輸出,可是在某一層渲染完成後,沒法在回過頭來處理或者改變其中的某個部分,由於在這以前的全部層像素數據,已經在渲染結束後被永久覆蓋/丟棄了。post
若是咱們想對某一層layer進行疊加/裁剪或者其餘複雜的操做,就不得不新開一塊內存區域,來處理這些些更加複雜的操做。性能
咱們看過一些文章有提到過CPU離屏渲染
,那麼什麼是CPU離屏渲染
呢?
若是咱們在UIView
中實現了-drawRect
方法,就算它的函數體內部實際沒有代碼,系統也會爲這個view
申請一塊內存區域,等待CoreGraphics
可能的繪畫操做。
這種狀況下,新開闢了一塊CGContext
,渲染數據暫時存儲在了CGContext
中,而沒有給到Frame Buffer
。根據上面的定義來講,沒有把渲染結果直接給到Frame Buffer
的,那就屬於離屏渲染
了。
可是,全部 CPU 執行的光柵化操做,好比圖片的解碼等等,都沒法直接繪製到 GPU 的Frame Buffer
中,多須要一塊用來中轉的內存區域。固然,咱們知道 CPU 並不擅長渲染,因此咱們應該儘可能避免使用 CPU 渲染。根據蘋果的說法,這並不是真正意義上的離屏渲染
,而且若是咱們重寫了-drawRect
方法,使用Xcode
檢測,也並不會被標記爲離屏渲染
。
在模擬器中經過設置 Color Off-Screen Rendered
來檢查哪些圖層觸發了離屏渲染。
觸發了離屏渲染的圖層會被標記爲黃色。
當須要裁切圖層的內容content
,很顯然這就須要開闢一塊內存來操做了。當只設置cornerRadius
時,不須要裁切內容,只須要一個帶圓角的邊框,則不會觸發離屏渲染。
陰影依賴layer自己的形狀等信息,而且根據畫家算法
,陰影須要先畫出來,這樣來講就須要在單獨的內存中先進行依賴的合成計算,再添加到Frame Buffer
,形成離屏渲染
。不過若是咱們可以預先告訴CoreAnimation
(經過shadowPath
屬性)陰影的幾何形狀,那麼陰影固然能夠先被獨立渲染出來,不須要依賴layer本體,也就再也不須要離屏渲染
了。
須要將一組圖層畫完以後,再總體加上alpha
,最後和底下其餘layer的像素進行組合。顯然也沒法經過一次遍歷就獲得最終結果。
咱們知道mask是應用在layer和其全部子layer的組合之上的,並且可能帶有透明度,那麼其實和group opacity
的原理相似,不得不在離屏渲染中完成。
渲染出毛玻璃效果,須要先畫出原圖層,而後capture
原圖層,進行水平模糊(Horizontal Blur)和垂直模糊(Vertical Blur),最後進行合成操做。顯然這須要在離屏緩衝區中完成。
shouldRasterize
一旦被設置爲YES
,Render Server
就會強制把layer的渲染結果(包括其子layer,以及圓角、陰影、group opacity
等等)保存在一塊內存中,這樣一來在下一幀仍然能夠被複用,而不會再次觸發離屏渲染。有幾個須要注意的點
shouldRasterize
的主旨在於下降性能損失,但老是至少會觸發一次離屏渲染。若是你的layer原本並不複雜,也沒有圓角陰影等等,則沒有必要打開shouldRasterize
shouldRasterize
,把layer繪製到一塊緩存,而後在接下來複用這個結果,這樣就不須要每次都從新繪製整個layer樹了2.5倍
大小,若是超出了會自動被丟棄,且沒法被複用了100ms
沒有被使用,會自動被丟棄,且沒法被複用了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]; 複製代碼
此時的確不會觸發離屏渲染,也達到了圓角的目的。
在實際項目中,通常會使用一張圖片做爲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]; 複製代碼
能夠看到,這裏仍然沒有發生離屏渲染。那麼離屏渲染到底和什麼有關係呢?
仍是上面的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]; 複製代碼
在同時設置了backgroundColor
和setImage:
以後,這裏觸發了離屏渲染。
關於性能優化,就是平衡 CPU 與 GPU 的負載工做,由於要作的事情就那麼多。當 GPU 忙不過來的時候,咱們能夠利用 CPU 的空閒來渲染而後提交給 GPU 顯示,來提升總體的渲染效率。渲染不是CPU的強項,調用CoreGraphics
會消耗其至關一部分計算時間,通常來講 CPU 渲染都在後臺線程完成(這也是AsyncDisplayKit
的主要思想),而後再回到主線程上,把渲染結果傳回CoreAnimation
。這樣一來,多線程間數據同步會增長必定的複雜度。CPU渲染速度不夠快,所以只適合渲染靜態的元素,如文字、圖片。做爲渲染結果的bitmap數量較大,很容易致使OOM。若是你選擇使用 CPU 來作渲染,那麼就沒有理由再觸發 GPU 的離屏渲染了。