前言
本篇文章是摘錄,為了紀(jì)錄學(xué)習(xí),文中講了什么是離屏渲染,以及開發(fā)中,出現(xiàn)禮品渲染的幾種情況,以及如何最大程度的去優(yōu)化和善用,僅供學(xué)習(xí)參考~
離屏渲染的定義
要在屏幕上顯示內(nèi)容,需要一塊玉屏幕像素?cái)?shù)據(jù)量一樣大的frame buffer, 作為像素?cái)?shù)據(jù)存儲(chǔ)區(qū)域,這也是GPU存儲(chǔ)渲染結(jié)果的地方。如果有時(shí)因?yàn)槊媾R一些限制,無(wú)法把渲染結(jié)果直接寫入frame buffer,而是暫時(shí)存在另外的內(nèi)存區(qū)域,之后寫入frame buffer, 那么這個(gè)過(guò)程被稱之為離屏渲染

CPU “離屏渲染”
如果我們?cè)赨IView中實(shí)現(xiàn)了drawRect方法,就算它的函數(shù)體內(nèi)部實(shí)際沒有代碼,系統(tǒng)也會(huì)為這個(gè)view申請(qǐng)一塊內(nèi)存區(qū)域,等待CoreGraphics可能的繪畫操作。
對(duì)于類似這種“新開一塊CGContext來(lái)畫圖“的操作,有很多文章和視頻也稱之為“離屏渲染”(因?yàn)橄袼財(cái)?shù)據(jù)是暫時(shí)存入了CGContext,而不是直接到了frame buffer)。進(jìn)一步來(lái)說(shuō),其實(shí)所有CPU進(jìn)行的光柵化操作(如文字渲染、圖片解碼),都無(wú)法直接繪制到由GPU掌管的frame buffer,只能暫時(shí)先放在另一塊內(nèi)存之中,說(shuō)起來(lái)都屬于“離屏渲染”。
自然我們會(huì)認(rèn)為,因?yàn)镃PU不擅長(zhǎng)做這件事,所以我們需要盡量避免它,就誤以為這就是需要避免離屏渲染的原因。但是根據(jù)蘋果工程師的說(shuō)法,CPU渲染并非真正意義上的離屏渲染。另一個(gè)證據(jù)是,如果你的view實(shí)現(xiàn)了drawRect,此時(shí)打開Xcode調(diào)試的“Color offscreen rendered yellow”開關(guān),你會(huì)發(fā)現(xiàn)這片區(qū)域不會(huì)被標(biāo)記為黃色,說(shuō)明Xcode并不認(rèn)為這屬于離屏渲染。
其實(shí)通過(guò)CPU渲染就是俗稱的“軟件渲染”,而真正的離屏渲染發(fā)生在GPU。
GPU離屏渲染
渲染操作都是由CoreAnimation的Render Server模塊,通過(guò)調(diào)用顯卡驅(qū)動(dòng)所提供的OpenGL/Metal接口來(lái)執(zhí)行的。通常對(duì)于每一層layer,Render Server會(huì)遵循“畫家算法[1]”,按次序輸出到frame buffer,后一層覆蓋前一層,就能得到最終的顯示結(jié)果(值得一提的是,與一般桌面架構(gòu)不同,在iOS中,設(shè)備主存和GPU的顯存共享物理內(nèi)存[2],這樣可以省去一些數(shù)據(jù)傳輸開銷)。
對(duì)于每一層layer,要么能找到一種通過(guò)單次遍歷就能完成渲染的算法,要么就不得不另開一塊內(nèi)存,借助這個(gè)臨時(shí)中轉(zhuǎn)區(qū)域來(lái)完成一些更復(fù)雜的、多次的修改/剪裁操作。
如果要繪制一個(gè)帶有圓角并剪切圓角以外內(nèi)容的容器,就會(huì)觸發(fā)離屏渲染。我的猜想是(如果讀者中有圖形學(xué)專家希望能指正):
? 將一個(gè)layer的內(nèi)容裁剪成圓角,可能不存在一次遍歷就能完成的方法
? 容器的子layer因?yàn)楦溉萜饔袌A角,那么也會(huì)需要被裁剪,而這時(shí)它們還在渲染隊(duì)列中排隊(duì),尚未被組合到一塊畫布上,自然也無(wú)法統(tǒng)一裁剪
此時(shí)我們就不得不開辟一塊獨(dú)立于frame buffer的空白內(nèi)存,先把容器以及其所有子layer依次畫好,然后把四個(gè)角“剪”成圓形,再把結(jié)果畫到frame buffer中。這就是GPU的離屏渲染。
常見離屏渲染場(chǎng)景分析
cornerRadius+clipsToBounds,原因就如同上面提到的,不得已只能另開一塊內(nèi)存來(lái)操作。而如果只是設(shè)置cornerRadius(如不需要剪切內(nèi)容,只需要一個(gè)帶圓角的邊框),或者只是需要裁掉矩形區(qū)域以外的內(nèi)容(雖然也是剪切,但是稍微想一下就可以發(fā)現(xiàn),對(duì)于純矩形而言,實(shí)現(xiàn)這個(gè)算法似乎并不需要另開內(nèi)存),并不會(huì)觸發(fā)離屏渲染。關(guān)于剪切圓角的性能優(yōu)化,根據(jù)場(chǎng)景不同有幾個(gè)方案可供選擇,非常推薦閱讀AsyncDisplayKit中的一篇文檔點(diǎn)我。
-
shadow ,原因在于,雖然layer本身是一塊矩形區(qū)域,但是陰影默認(rèn)是作用在其中"非透明區(qū)域"的,而且需要顯示在所有l(wèi)ayer內(nèi)容的下方,因?yàn)?strong>此時(shí)陰影的本體(layer和其子layer)都還沒有被組合到一起,所以不能在第一步就畫出只有完成最后一步之后才能知道的形狀
這樣一來(lái)又只能另外申請(qǐng)一塊內(nèi)存,把本體內(nèi)容都先畫好,再根據(jù)渲染結(jié)果的形狀,添加陰影到frame buffer,最后把內(nèi)容畫上去(這只是我的猜測(cè),實(shí)際情況可能更復(fù)雜)。不過(guò)如果我們能夠預(yù)先告訴CoreAnimation(通過(guò)shadowPath屬性)陰影的幾何形狀,那么陰影當(dāng)然可以先被獨(dú)立渲染出來(lái),不需要依賴layer本體,也就不再需要離屏渲染了。
image.png -
group opacity,其實(shí)從名字就可以猜到,alpha并不是分別應(yīng)用在每一層之上,而是只有到整個(gè)layer樹畫完之后,再統(tǒng)一加上alpha,最后和底下其他layer的像素進(jìn)行組合。顯然也無(wú)法通過(guò)一次遍歷就得到最終結(jié)果。將一對(duì)藍(lán)色和紅色layer疊在一起,然后在父layer上設(shè)置opacity=0.5,并復(fù)制一份在旁邊作對(duì)比。左邊關(guān)閉group opacity,右邊保持默認(rèn)(從iOS7開始,如果沒有顯式指定,group opacity會(huì)默認(rèn)打開),然后打開offscreen rendering的調(diào)試,我們會(huì)發(fā)現(xiàn)右邊的那一組確實(shí)是離屏渲染了。
image.png -
mask,我們知道m(xù)ask是應(yīng)用在layer和其所有子layer的組合之上的,而且可能帶有透明度,那么其實(shí)和group opacity的原理類似,不得不在離屏渲染中完成。
image.png -
UIBlurEffect,同樣無(wú)法通過(guò)一次遍歷完成,其原理在WWDC中提到
image.png 其他還有一些,類似allowsEdgeAntialiasing等等也可能會(huì)觸發(fā)離屏渲染,原理也都是類似:如果你無(wú)法僅僅使用frame buffer來(lái)畫出最終結(jié)果,那就只能另開一塊內(nèi)存空間來(lái)儲(chǔ)存中間結(jié)果。這些原理并不神秘。
GPU離屏渲染的性能影響
GPU的操作是高度流水線化的。本來(lái)所有計(jì)算工作都在有條不紊地正在向frame buffer輸出,此時(shí)突然收到指令,需要輸出到另一塊內(nèi)存,那么流水線中正在進(jìn)行的一切都不得不被丟棄,切換到只能服務(wù)于我們當(dāng)前的“切圓角”操作。等到完成以后再次清空,再回到向frame buffer輸出的正常流程。
在tableView或者collectionView中,滾動(dòng)的每一幀變化都會(huì)觸發(fā)每個(gè)cell的重新繪制,因此一旦存在離屏渲染,上面提到的上下文切換就會(huì)每秒發(fā)生60次,并且很可能每一幀有幾十張的圖片要求這么做,對(duì)于GPU的性能沖擊可想而知(GPU非常擅長(zhǎng)大規(guī)模并行計(jì)算,但是我想頻繁的上下文切換顯然不在其設(shè)計(jì)考量之中)
善用離屏渲染,提升性能
盡管離屏渲染開銷很大,但是當(dāng)我們無(wú)法避免它的時(shí)候,可以想辦法把性能影響降到最低。優(yōu)化思路也很簡(jiǎn)單:既然已經(jīng)花了不少精力把圖片裁出了圓角,如果我能把結(jié)果緩存下來(lái),那么下一幀渲染就可以復(fù)用這個(gè)成果,不需要再重新畫一遍了。
CALayer為這個(gè)方案提供了對(duì)應(yīng)的解法:shouldRasterize。一旦被設(shè)置為true,Render Server就會(huì)強(qiáng)制把layer的渲染結(jié)果(包括其子layer,以及圓角、陰影、group opacity等等)保存在一塊內(nèi)存中,這樣一來(lái)在下一幀仍然可以被復(fù)用,而不會(huì)再次觸發(fā)離屏渲染。有幾個(gè)需要注意的點(diǎn):
shouldRasterize的主旨在于降低性能損失,但總是至少會(huì)觸發(fā)一次離屏渲染。如果你的layer本來(lái)并不復(fù)雜,也沒有圓角陰影等等,打開這個(gè)開關(guān)反而會(huì)增加一次不必要的離屏渲染
? 離屏渲染緩存有空間上限,最多不超過(guò)屏幕總像素的2.5倍大小
? 一旦緩存超過(guò)100ms沒有被使用,會(huì)自動(dòng)被丟棄
? layer的內(nèi)容(包括子layer)必須是靜態(tài)的,因?yàn)橐坏┌l(fā)生變化(如resize,動(dòng)畫),之前辛苦處理得到的緩存就失效了。如果這件事頻繁發(fā)生,我們就又回到了“每一幀都需要離屏渲染”的情景,而這正是開發(fā)者需要極力避免的。針對(duì)這種情況,Xcode提供了“Color Hits Green and Misses Red”的選項(xiàng),幫助我們查看緩存的使用是否符合預(yù)期
? 其實(shí)除了解決多次離屏渲染的開銷,shouldRasterize在另一個(gè)場(chǎng)景中也可以使用:如果layer的子結(jié)構(gòu)非常復(fù)雜,渲染一次所需時(shí)間較長(zhǎng),同樣可以打開這個(gè)開關(guān),把layer繪制到一塊緩存,然后在接下來(lái)復(fù)用這個(gè)結(jié)果,這樣就不需要每次都重新繪制整個(gè)layer樹了
什么時(shí)候需要CPU渲染
渲染性能的調(diào)優(yōu),其實(shí)始終是在做一件事:平衡CPU和GPU的負(fù)載,讓他們盡量做各自最擅長(zhǎng)的工作。
絕大多數(shù)情況下,得益于GPU針對(duì)圖形處理的優(yōu)化,我們都會(huì)傾向于讓GPU來(lái)完成渲染任務(wù),而給CPU留出足夠時(shí)間處理各種各樣復(fù)雜的App邏輯。為此Core Animation做了大量的工作,盡量把渲染工作轉(zhuǎn)換成適合GPU處理的形式(也就是所謂的硬件加速,如layer composition,設(shè)置backgroundColor等等)。
但是對(duì)于一些情況,如文字(CoreText使用CoreGraphics渲染)和圖片(ImageIO)渲染,由于GPU并不擅長(zhǎng)做這些工作,不得不先由CPU來(lái)處理好以后,再把結(jié)果作為texture傳給GPU。除此以外,有時(shí)候也會(huì)遇到GPU實(shí)在忙不過(guò)來(lái)的情況,而CPU相對(duì)空閑(GPU瓶頸),這時(shí)可以讓CPU分擔(dān)一部分工作,提高整體效率。

一個(gè)典型的例子是,我們經(jīng)常會(huì)使用CoreGraphics給圖片加上圓角(將圖片中圓角以外的部分渲染成透明)。整個(gè)過(guò)程全部是由CPU完成的。這樣一來(lái)既然我們已經(jīng)得到了想要的效果,就不需要再另外給圖片容器設(shè)置cornerRadius。另一個(gè)好處是,我們可以靈活地控制裁剪和緩存的時(shí)機(jī),巧妙避開CPU和GPU最繁忙的時(shí)段,達(dá)到平滑性能波動(dòng)的目的。
這里有幾個(gè)需要注意的點(diǎn):
? 渲染不是CPU的強(qiáng)項(xiàng),調(diào)用CoreGraphics會(huì)消耗其相當(dāng)一部分計(jì)算時(shí)間,并且我們也不愿意因此阻塞用戶操作,因此一般來(lái)說(shuō)CPU渲染都在后臺(tái)線程完成(這也是AsyncDisplayKit的主要思想),然后再回到主線程上,把渲染結(jié)果傳回CoreAnimation。這樣一來(lái),多線程間數(shù)據(jù)同步會(huì)增加一定的復(fù)雜度
? 同樣因?yàn)镃PU渲染速度不夠快,因此只適合渲染靜態(tài)的元素,如文字、圖片(想象一下沒有硬件加速的視頻解碼,性能慘不忍睹)
? 作為渲染結(jié)果的bitmap數(shù)據(jù)量較大(形式上一般為解碼后的UIImage),消耗內(nèi)存較多,所以應(yīng)該在使用完及時(shí)釋放,并在需要的時(shí)候重新生成,否則很容易導(dǎo)致OOM
? 如果你選擇使用CPU來(lái)做渲染,那么就沒有理由再觸發(fā)GPU的離屏渲染了,否則會(huì)同時(shí)存在兩塊內(nèi)容相同的內(nèi)存,而且CPU和GPU都會(huì)比較辛苦
? 一定要使用Instruments的不同工具來(lái)測(cè)試性能,而不是僅憑猜測(cè)來(lái)做決定
針對(duì)離屏渲染,如何優(yōu)化
由于在iOS10之后,系統(tǒng)的設(shè)計(jì)風(fēng)格慢慢從扁平化轉(zhuǎn)變成圓角卡片,即刻的設(shè)計(jì)風(fēng)格也隨之發(fā)生變化,加入了大量圓角與陰影效果,如果在處理上稍有不慎,就很容易觸發(fā)離屏渲染。為此我們采取了以下一些措施:
? 即刻大量應(yīng)用AsyncDisplayKit(Texture)作為主要渲染框架,對(duì)于文字和圖片的異步渲染操作交由框架來(lái)處理。關(guān)于這方面可以看我之前的一些介紹
? 對(duì)于圖片的圓角,統(tǒng)一采用“precomposite”的策略,也就是不經(jīng)由容器來(lái)做剪切,而是預(yù)先使用CoreGraphics為圖片裁剪圓角
? 對(duì)于視頻的圓角,由于實(shí)時(shí)剪切非常消耗性能,我們會(huì)創(chuàng)建四個(gè)白色弧形的layer蓋住四個(gè)角,從視覺上制造圓角的效果
? 對(duì)于view的圓形邊框,如果沒有backgroundColor,可以放心使用cornerRadius來(lái)做
? 對(duì)于所有的陰影,使用shadowPath來(lái)規(guī)避離屏渲染
//這行代碼制定了陰影路徑,如果沒有手動(dòng)指定,Core Animation會(huì)去自動(dòng)計(jì)算,這就會(huì)觸發(fā)離屏渲染。如果人為指定了陰影路徑,就可以免去計(jì)算,從而避免產(chǎn)生離屏渲染。
imgView.layer.shadowPath = UIBezierPath(rect: imgView.bounds).CGPath
? 對(duì)于特殊形狀的view,使用layer mask并打開shouldRasterize來(lái)對(duì)渲染結(jié)果進(jìn)行緩存
? 對(duì)于模糊效果,不采用系統(tǒng)提供的UIVisualEffect,而是另外實(shí)現(xiàn)模糊效果(CIGaussianBlur),并手動(dòng)管理渲染結(jié)果
本文摘錄參考



