本篇文章是摘錄,文中講了什麼是離屏渲染,以及開發中,出現離屏渲染的幾種狀況,以及如何最大程度的去優化和藹用,僅供學習參考~html
要在屏幕上顯示內容,須要一塊玉屏幕像素數據量同樣大的frame buffer, 做爲像素數據存儲區域,這也是GPU存儲渲染結果的地方。若是有時由於面臨一些限制,沒法把渲染結果直接寫入frame buffer,而是暫時存在另外的內存區域,以後寫入frame buffer, 那麼這個過程被稱之爲離屏渲染算法
若是咱們在UIView中實現了drawRect方法,就算它的函數體內部實際沒有代碼,系統也會爲這個view申請一塊內存區域,等待CoreGraphics可能的繪畫操做。緩存
對於相似這種「新開一塊CGContext來畫圖「的操做,有不少文章和視頻也稱之爲「離屏渲染」(由於像素數據是暫時存入了CGContext,而不是直接到了frame buffer)。進一步來講,其實全部CPU進行的光柵化操做(如文字渲染、圖片解碼),都沒法直接繪製到由GPU掌管的frame buffer,只能暫時先放在另外一塊內存之中,提及來都屬於「離屏渲染」。性能優化
天然咱們會認爲,由於CPU不擅長作這件事,因此咱們須要儘可能避免它,就誤覺得這就是須要避免離屏渲染的緣由。可是根據蘋果工程師的說法,CPU渲染並不是真正意義上的離屏渲染。另外一個證據是,若是你的view實現了drawRect,此時打開Xcode調試的「Color offscreen rendered yellow」開關,你會發現這片區域不會被標記爲黃色,說明Xcode並不認爲這屬於離屏渲染。多線程
其實經過CPU渲染就是俗稱的「軟件渲染」,而真正的離屏渲染髮生在GPU。架構
渲染操做都是由CoreAnimation的Render Server模塊,經過調用顯卡驅動所提供的OpenGL/Metal接口來執行的。一般對於每一層layer,Render Server會遵循「畫家算法[1]」,按次序輸出到frame buffer,後一層覆蓋前一層,就能獲得最終的顯示結果(值得一提的是,與通常桌面架構不一樣,在iOS中,設備主存和GPU的顯存共享物理內存[2],這樣能夠省去一些數據傳輸開銷)。框架
對於每一層layer,要麼能找到一種經過單次遍歷就能完成渲染的算法,要麼就不得不另開一塊內存,藉助這個臨時中轉區域來完成一些更復雜的、屢次的修改/剪裁操做。異步
若是要繪製一個帶有圓角並剪切圓角之外內容的容器,就會觸發離屏渲染。個人猜測是(若是讀者中有圖形學專家但願能指正):函數
• 將一個layer的內容裁剪成圓角,可能不存在一次遍歷就能完成的方法工具
• 容器的子layer由於父容器有圓角,那麼也會須要被裁剪,而這時它們還在渲染隊列中排隊,還沒有被組合到一塊畫布上,天然也沒法統一裁剪
此時咱們就不得不開闢一塊獨立於frame buffer的空白內存,先把容器以及其全部子layer依次畫好,而後把四個角「剪」成圓形,再把結果畫到frame buffer中。這就是GPU的離屏渲染。
cornerRadius+clipsToBounds,緣由就如同上面提到的,不得已只能另開一塊內存來操做。而若是隻是設置cornerRadius(如不須要剪切內容,只須要一個帶圓角的邊框),或者只是須要裁掉矩形區域之外的內容(雖然也是剪切,可是稍微想一下就能夠發現,對於純矩形而言,實現這個算法彷佛並不須要另開內存),並不會觸發離屏渲染。關於剪切圓角的性能優化,根據場景不一樣有幾個方案可供選擇,很是推薦閱讀AsyncDisplayKit中的一篇文檔點我。
shadow ,緣由在於,雖然layer自己是一塊矩形區域,可是陰影默認是做用在其中"非透明區域"的,並且須要顯示在全部layer內容的下方,由於此時陰影的本體(layer和其子layer)都尚未被組合到一塊兒,因此不能在第一步就畫出只有完成最後一步以後才能知道的形狀
這樣一來又只能另外申請一塊內存,把本體內容都先畫好,再根據渲染結果的形狀,添加陰影到frame buffer,最後把內容畫上去(這只是個人猜想,實際狀況可能更復雜)。不過若是咱們可以預先告訴CoreAnimation(經過shadowPath屬性)陰影的幾何形狀,那麼陰影固然能夠先被獨立渲染出來,不須要依賴layer本體,也就再也不須要離屏渲染了。
group opacity,其實從名字就能夠猜到,alpha並非分別應用在每一層之上,而是隻有到整個layer樹畫完以後,再統一加上alpha,最後和底下其餘layer的像素進行組合。顯然也沒法經過一次遍歷就獲得最終結果。將一對藍色和紅色layer疊在一塊兒,而後在父layer上設置opacity=0.5,並複製一份在旁邊做對比。左邊關閉group opacity,右邊保持默認(從iOS7開始,若是沒有顯式指定,group opacity會默認打開),而後打開offscreen rendering的調試,咱們會發現右邊的那一組確實是離屏渲染了。
mask,咱們知道mask是應用在layer和其全部子layer的組合之上的,並且可能帶有透明度,那麼其實和group opacity的原理相似,不得不在離屏渲染中完成。
UIBlurEffect,一樣沒法經過一次遍歷完成,其原理在WWDC中提到
其餘還有一些,相似allowsEdgeAntialiasing等等也可能會觸發離屏渲染,原理也都是相似:若是你沒法僅僅使用frame buffer來畫出最終結果,那就只能另開一塊內存空間來儲存中間結果。這些原理並不神祕。
GPU的操做是高度流水線化的。原本全部計算工做都在有條不紊地正在向frame buffer輸出,此時忽然收到指令,須要輸出到另外一塊內存,那麼流水線中正在進行的一切都不得不被丟棄,切換到只能服務於咱們當前的「切圓角」操做。等到完成之後再次清空,再回到向frame buffer輸出的正常流程。
在tableView或者collectionView中,滾動的每一幀變化都會觸發每一個cell的從新繪製,所以一旦存在離屏渲染,上面提到的上下文切換就會每秒發生60次,而且極可能每一幀有幾十張的圖片要求這麼作,對於GPU的性能衝擊可想而知(GPU很是擅長大規模並行計算,可是我想頻繁的上下文切換顯然不在其設計考量之中)
儘管離屏渲染開銷很大,可是當咱們沒法避免它的時候,能夠想辦法把性能影響降到最低。優化思路也很簡單:既然已經花了很多精力把圖片裁出了圓角,若是我能把結果緩存下來,那麼下一幀渲染就能夠複用這個成果,不須要再從新畫一遍了。
CALayer爲這個方案提供了對應的解法:shouldRasterize。一旦被設置爲true,Render Server就會強制把layer的渲染結果(包括其子layer,以及圓角、陰影、group opacity等等)保存在一塊內存中,這樣一來在下一幀仍然能夠被複用,而不會再次觸發離屏渲染。有幾個須要注意的點:
shouldRasterize的主旨在於下降性能損失,但老是至少會觸發一次離屏渲染。若是你的layer原本並不複雜,也沒有圓角陰影等等,打開這個開關反而會增長一次沒必要要的離屏渲染
• 離屏渲染緩存有空間上限,最多不超過屏幕總像素的2.5倍大小
• 一旦緩存超過100ms沒有被使用,會自動被丟棄
• layer的內容(包括子layer)必須是靜態的,由於一旦發生變化(如resize,動畫),以前辛苦處理獲得的緩存就失效了。若是這件事頻繁發生,咱們就又回到了「每一幀都須要離屏渲染」的情景,而這正是開發者須要極力避免的。針對這種狀況,Xcode提供了「Color Hits Green and Misses Red」的選項,幫助咱們查看緩存的使用是否符合預期
• 其實除了解決屢次離屏渲染的開銷,shouldRasterize在另外一個場景中也可使用:若是layer的子結構很是複雜,渲染一次所需時間較長,一樣能夠打開這個開關,把layer繪製到一塊緩存,而後在接下來複用這個結果,這樣就不須要每次都從新繪製整個layer樹了
渲染性能的調優,其實始終是在作一件事:平衡CPU和GPU的負載,讓他們儘可能作各自最擅長的工做。
絕大多數狀況下,得益於GPU針對圖形處理的優化,咱們都會傾向於讓GPU來完成渲染任務,而給CPU留出足夠時間處理各類各樣複雜的App邏輯。爲此Core Animation作了大量的工做,儘可能把渲染工做轉換成適合GPU處理的形式(也就是所謂的硬件加速,如layer composition,設置backgroundColor等等)。
可是對於一些狀況,如文字(CoreText使用CoreGraphics渲染)和圖片(ImageIO)渲染,因爲GPU並不擅長作這些工做,不得不先由CPU來處理好之後,再把結果做爲texture傳給GPU。除此之外,有時候也會遇到GPU實在忙不過來的狀況,而CPU相對空閒(GPU瓶頸),這時可讓CPU分擔一部分工做,提升總體效率。
一個典型的例子是,咱們常常會使用CoreGraphics給圖片加上圓角(將圖片中圓角之外的部分渲染成透明)。整個過程所有是由CPU完成的。這樣一來既然咱們已經獲得了想要的效果,就不須要再另外給圖片容器設置cornerRadius。另外一個好處是,咱們能夠靈活地控制裁剪和緩存的時機,巧妙避開CPU和GPU最繁忙的時段,達到平滑性能波動的目的。
這裏有幾個須要注意的點:
• 渲染不是CPU的強項,調用CoreGraphics會消耗其至關一部分計算時間,而且咱們也不肯意所以阻塞用戶操做,所以通常來講CPU渲染都在後臺線程完成(這也是AsyncDisplayKit的主要思想),而後再回到主線程上,把渲染結果傳回CoreAnimation。這樣一來,多線程間數據同步會增長必定的複雜度
• 一樣由於CPU渲染速度不夠快,所以只適合渲染靜態的元素,如文字、圖片(想象一下沒有硬件加速的視頻解碼,性能慘不忍睹)
• 做爲渲染結果的bitmap數據量較大(形式上通常爲解碼後的UIImage),消耗內存較多,因此應該在使用完及時釋放,並在須要的時候從新生成,不然很容易致使OOM
• 若是你選擇使用CPU來作渲染,那麼就沒有理由再觸發GPU的離屏渲染了,不然會同時存在兩塊內容相同的內存,並且CPU和GPU都會比較辛苦
• 必定要使用Instruments的不一樣工具來測試性能,而不是僅憑猜想來作決定
因爲在iOS10以後,系統的設計風格慢慢從扁平化轉變成圓角卡片,即刻的設計風格也隨之發生變化,加入了大量圓角與陰影效果,若是在處理上稍有不慎,就很容易觸發離屏渲染。爲此咱們採起了如下一些措施:
• 即刻大量應用AsyncDisplayKit(Texture)做爲主要渲染框架,對於文字和圖片的異步渲染操做交由框架來處理。關於這方面能夠看我以前的一些介紹
• 對於圖片的圓角,統一採用「precomposite」的策略,也就是不經由容器來作剪切,而是預先使用CoreGraphics爲圖片裁剪圓角
• 對於視頻的圓角,因爲實時剪切很是消耗性能,咱們會建立四個白色弧形的layer蓋住四個角,從視覺上製造圓角的效果
• 對於view的圓形邊框,若是沒有backgroundColor,能夠放心使用cornerRadius來作
• 對於全部的陰影,使用shadowPath來規避離屏渲染
• 對於特殊形狀的view,使用layer mask並打開shouldRasterize來對渲染結果進行緩存
• 對於模糊效果,不採用系統提供的UIVisualEffect,而是另外實現模糊效果(CIGaussianBlur),並手動管理渲染結果