在WWDC的Advanced Graphics and Animations for iOS Apps(WWDC14 419,關(guān)于UIKit和Core Animation基礎(chǔ)的session在早年的WWDC中比較多)中有這樣一張圖:
我們可以看到,在Application這一層中主要是CPU在操作,而到了Render Server這一層,CoreAnimation會(huì)將具體操作轉(zhuǎn)換成發(fā)送給GPU的draw calls(以前是call OpenGL ES,現(xiàn)在慢慢轉(zhuǎn)到了Metal),顯然CPU和GPU雙方同處于一個(gè)流水線中,協(xié)作完成整個(gè)渲染工作。
如果要在顯示屏上顯示內(nèi)容,我們至少需要一塊與屏幕像素?cái)?shù)據(jù)量一樣大的frame buffer,作為像素?cái)?shù)據(jù)存儲(chǔ)區(qū)域,而這也是GPU存儲(chǔ)渲染結(jié)果的地方。如果有時(shí)因?yàn)槊媾R一些限制,無法把渲染結(jié)果直接寫入frame buffer,而是先暫存在另外的內(nèi)存區(qū)域,之后再寫入frame buffer,那么這個(gè)過程被稱之為離屏渲染。
大家知道,如果我們?cè)赨IView中實(shí)現(xiàn)了drawRect方法,就算它的函數(shù)體內(nèi)部實(shí)際沒有代碼,系統(tǒng)也會(huì)為這個(gè)view申請(qǐng)一塊內(nèi)存區(qū)域,等待CoreGraphics可能的繪畫操作。
對(duì)于類似這種“新開一塊CGContext來畫圖“的操作,有很多文章和視頻也稱之為“離屏渲染”(因?yàn)橄袼財(cái)?shù)據(jù)是暫時(shí)存入了CGContext,而不是直接到了frame buffer)。進(jìn)一步來說,其實(shí)所有CPU進(jìn)行的光柵化操作(如文字渲染、圖片解碼),都無法直接繪制到由GPU掌管的frame buffer,只能暫時(shí)先放在另一塊內(nèi)存之中,說起來都屬于“離屏渲染”。
自然我們會(huì)認(rèn)為,因?yàn)镃PU不擅長(zhǎng)做這件事,所以我們需要盡量避免它,就誤以為這就是需要避免離屏渲染的原因。但是根據(jù)蘋果工程師的說法,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)記為黃色,說明Xcode并不認(rèn)為這屬于離屏渲染。
其實(shí)通過CPU渲染就是俗稱的“軟件渲染”,而真正的離屏渲染發(fā)生在GPU。
在上面的渲染流水線示意圖中我們可以看到,主要的渲染操作都是由CoreAnimation的Render Server模塊,通過調(diào)用顯卡驅(qū)動(dòng)所提供的OpenGL/Metal接口來執(zhí)行的。通常對(duì)于每一層layer,Render Server會(huì)遵循“畫家算法”,按次序輸出到frame buffer,后一層覆蓋前一層,就能得到最終的顯示結(jié)果(值得一提的是,與一般桌面架構(gòu)不同,在iOS中,設(shè)備主存和GPU的顯存共享物理內(nèi)存,這樣可以省去一些數(shù)據(jù)傳輸開銷)。
然而有些場(chǎng)景并沒有那么簡(jiǎn)單。作為“畫家”的GPU雖然可以一層一層往畫布上進(jìn)行輸出,但是無法在某一層渲染完成之后,再回過頭來擦除/改變其中的某個(gè)部分——因?yàn)樵谶@一層之前的若干層layer像素?cái)?shù)據(jù),已經(jīng)在渲染中被永久覆蓋了。這就意味著,對(duì)于每一層layer,要么能找到一種通過單次遍歷就能完成渲染的算法,要么就不得不另開一塊內(nèi)存,借助這個(gè)臨時(shí)中轉(zhuǎn)區(qū)域來完成一些更復(fù)雜的、多次的修改/剪裁操作。
如果要繪制一個(gè)帶有圓角并剪切圓角以外內(nèi)容的容器,就會(huì)觸發(fā)離屏渲染。我的猜想是(如果讀者中有圖形學(xué)專家希望能指正):
此時(shí)我們就不得不開辟一塊獨(dú)立于frame buffer的空白內(nèi)存,先把容器以及其所有子layer依次畫好,然后把四個(gè)角“剪”成圓形,再把結(jié)果畫到frame buffer中。這就是GPU的離屏渲染。
GPU的操作是高度流水線化的。本來所有計(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)我們無法避免它的時(shí)候,可以想辦法把性能影響降到最低。優(yōu)化思路也很簡(jiǎn)單:既然已經(jīng)花了不少精力把圖片裁出了圓角,如果我能把結(jié)果緩存下來,那么下一幀渲染就可以復(fù)用這個(gè)成果,不需要再重新畫一遍了。
CALayer為這個(gè)方案提供了對(duì)應(yīng)的解法:shouldRasterize。一旦被設(shè)置為true,Render Server就會(huì)強(qiáng)制把layer的渲染結(jié)果(包括其子layer,以及圓角、陰影、group opacity等等)保存在一塊內(nèi)存中,這樣一來在下一幀仍然可以被復(fù)用,而不會(huì)再次觸發(fā)離屏渲染。有幾個(gè)需要注意的點(diǎn):
渲染性能的調(diào)優(yōu),其實(shí)始終是在做一件事:平衡CPU和GPU的負(fù)載,讓他們盡量做各自最擅長(zhǎng)的工作。
絕大多數(shù)情況下,得益于GPU針對(duì)圖形處理的優(yōu)化,我們都會(huì)傾向于讓GPU來完成渲染任務(wù),而給CPU留出足夠時(shí)間處理各種各樣復(fù)雜的App邏輯。為此Core Animation做了大量的工作,盡量把渲染工作轉(zhuǎn)換成適合GPU處理的形式(也就是所謂的硬件加速,如layer composition,設(shè)置backgroundColor等等)。
但是對(duì)于一些情況,如文字(CoreText使用CoreGraphics渲染)和圖片(ImageIO)渲染,由于GPU并不擅長(zhǎng)做這些工作,不得不先由CPU來處理好以后,再把結(jié)果作為texture傳給GPU。除此以外,有時(shí)候也會(huì)遇到GPU實(shí)在忙不過來的情況,而CPU相對(duì)空閑(GPU瓶頸),這時(shí)可以讓CPU分擔(dān)一部分工作,提高整體效率。
一個(gè)典型的例子是,我們經(jīng)常會(huì)使用CoreGraphics給圖片加上圓角(將圖片中圓角以外的部分渲染成透明)。整個(gè)過程全部是由CPU完成的。這樣一來既然我們已經(jīng)得到了想要的效果,就不需要再另外給圖片容器設(shè)置cornerRadius。另一個(gè)好處是,我們可以靈活地控制裁剪和緩存的時(shí)機(jī),巧妙避開CPU和GPU最繁忙的時(shí)段,達(dá)到平滑性能波動(dòng)的目的。
這里有幾個(gè)需要注意的點(diǎn):
由于在iOS10之后,系統(tǒng)的設(shè)計(jì)風(fēng)格慢慢從扁平化轉(zhuǎn)變成圓角卡片,即刻的設(shè)計(jì)風(fēng)格也隨之發(fā)生變化,加入了大量圓角與陰影效果,如果在處理上稍有不慎,就很容易觸發(fā)離屏渲染。為此我們采取了以下一些措施:
離屏渲染牽涉了很多Core Animation、GPU和圖形學(xué)等等方面的知識(shí),在實(shí)踐中也非??简?yàn)一個(gè)工程師排查問題的基本功、經(jīng)驗(yàn)和判斷能力——如果在不恰當(dāng)?shù)臅r(shí)候打開了shouldRasterize,只會(huì)弄巧成拙。