无码人妻精一区二区三区,eeuss影院www在线观看,无码精品久久久久久人妻中字,日韩av高清在线看片

推薦新聞
關(guān)于iOS離屏渲染的深入研究
發(fā)布者:深藍(lán)互聯(lián)
發(fā)布時(shí)間:2019-07-24
點(diǎn)擊:次

iOS渲染架構(gòu)

在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è)過程被稱之為離屏渲染。

渲染結(jié)果先經(jīng)過了離屏buffer,再到frame buffer

CPU”離屏渲染“

大家知道,如果我們?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。

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é)專家希望能指正):

  • 將一個(gè)layer的內(nèi)容裁剪成圓角,可能不存在一次遍歷就能完成的方法
  • 容器的子layer因?yàn)楦溉萜饔袌A角,那么也會(huì)需要被裁剪,而這時(shí)它們還在渲染隊(duì)列中排隊(duì),尚未被組合到一塊畫布上,自然也無法統(tǒng)一裁剪

此時(shí)我們就不得不開辟一塊獨(dú)立于frame buffer的空白內(nèi)存,先把容器以及其所有子layer依次畫好,然后把四個(gè)角“剪”成圓形,再把結(jié)果畫到frame buffer中。這就是GPU的離屏渲染。

常見離屏渲染場(chǎng)景分析

  • cornerRadius+clipsToBounds,原因就如同上面提到的,不得已只能另開一塊內(nè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中的一篇文檔。
ASDK中對(duì)于如何選擇圓角渲染策略的流程圖,非常實(shí)用
  • shadow,其原因在于,雖然layer本身是一塊矩形區(qū)域,但是陰影默認(rèn)是作用在其中”非透明區(qū)域“的,而且需要顯示在所有l(wèi)ayer內(nèi)容的下方,因此根據(jù)畫家算法必須被渲染在先。但矛盾在于此時(shí)陰影的本體(layer和其子layer)都還沒有被組合到一起,怎么可能在第一步就畫出只有完成最后一步之后才能知道的形狀呢?這樣一來又只能另外申請(qǐng)一塊內(nèi)存,把本體內(nèi)容都先畫好,再根據(jù)渲染結(jié)果的形狀,添加陰影到frame buffer,最后把內(nèi)容畫上去(這只是我的猜測(cè),實(shí)際情況可能更復(fù)雜)。不過如果我們能夠預(yù)先告訴CoreAnimation(通過shadowPath屬性)陰影的幾何形狀,那么陰影當(dāng)然可以先被獨(dú)立渲染出來,不需要依賴layer本體,也就不再需要離屏渲染了。
陰影會(huì)作用在所有子layer所組成的形狀上,那就只能等全部子layer畫完才能得到
  • group opacity,其實(shí)從名字就可以猜到,alpha并不是分別應(yīng)用在每一層之上,而是只有到整個(gè)layer樹畫完之后,再統(tǒng)一加上alpha,最后和底下其他layer的像素進(jìn)行組合。顯然也無法通過一次遍歷就得到最終結(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í)是離屏渲染了。
同樣的兩個(gè)view,右邊打開group opacity(默認(rèn)行為)的被標(biāo)記為Offscreen rendering
  • mask,我們知道m(xù)ask是應(yīng)用在layer和其所有子layer的組合之上的,而且可能帶有透明度,那么其實(shí)和group opacity的原理類似,不得不在離屏渲染中完成。
WWDC中蘋果的解釋,mask需要遍歷至少三次
  • UIBlurEffect,同樣無法通過一次遍歷完成,其原理在WWDC中提到:
  • 其他還有一些,類似allowsEdgeAntialiasing等等也可能會(huì)觸發(fā)離屏渲染,原理也都是類似:如果你無法僅僅使用frame buffer來畫出最終結(jié)果,那就只能另開一塊內(nèi)存空間來儲(chǔ)存中間結(jié)果。這些原理并不神秘。

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ì)考量之中)

每16ms就需要根據(jù)當(dāng)前滾動(dòng)位置渲染整個(gè)tableView,是個(gè)不小的性能挑戰(zhàn)

善用離屏渲染

盡管離屏渲染開銷很大,但是當(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):

  • shouldRasterize的主旨在于降低性能損失,但總是至少會(huì)觸發(fā)一次離屏渲染。如果你的layer本來并不復(fù)雜,也沒有圓角陰影等等,打開這個(gè)開關(guān)反而會(huì)增加一次不必要的離屏渲染
  • 離屏渲染緩存有空間上限,最多不超過屏幕總像素的2.5倍大小
  • 一旦緩存超過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繪制到一塊緩存,然后在接下來復(fù)用這個(gè)結(jié)果,這樣就不需要每次都重新繪制整個(gè)layer樹了

什么時(shí)候需要CPU渲染

渲染性能的調(diào)優(yōu),其實(shí)始終是在做一件事:平衡CPU和GPU的負(fù)載,讓他們盡量做各自最擅長(zhǎng)的工作。

平衡CPU和GPU的負(fù)載

絕大多數(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)一部分工作,提高整體效率。

來自WWDC18 session 221,可以看到Core Text基于Core Graphics

一個(gè)典型的例子是,我們經(jīng)常會(huì)使用CoreGraphics給圖片加上圓角(將圖片中圓角以外的部分渲染成透明)。整個(gè)過程全部是由CPU完成的。這樣一來既然我們已經(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í)間,并且我們也不愿意因此阻塞用戶操作,因此一般來說CPU渲染都在后臺(tái)線程完成(這也是AsyncDisplayKit的主要思想),然后再回到主線程上,把渲染結(jié)果傳回CoreAnimation。這樣一來,多線程間數(shù)據(jù)同步會(huì)增加一定的復(fù)雜度
  • 同樣因?yàn)镃PU渲染速度不夠快,因此只適合渲染靜態(tài)的元素,如文字、圖片(想象一下沒有硬件加速的視頻解碼,性能慘不忍睹)
  • 作為渲染結(jié)果的bitmap數(shù)據(jù)量較大(形式上一般為解碼后的UIImage),消耗內(nèi)存較多,所以應(yīng)該在使用完及時(shí)釋放,并在需要的時(shí)候重新生成,否則很容易導(dǎo)致OOM
  • 如果你選擇使用CPU來做渲染,那么就沒有理由再觸發(fā)GPU的離屏渲染了,否則會(huì)同時(shí)存在兩塊內(nèi)容相同的內(nèi)存,而且CPU和GPU都會(huì)比較辛苦
  • 一定要使用Instruments的不同工具來測(cè)試性能,而不是僅憑猜測(cè)來做決定

即刻的優(yōu)化

由于在iOS10之后,系統(tǒng)的設(shè)計(jì)風(fēng)格慢慢從扁平化轉(zhuǎn)變成圓角卡片,即刻的設(shè)計(jì)風(fēng)格也隨之發(fā)生變化,加入了大量圓角與陰影效果,如果在處理上稍有不慎,就很容易觸發(fā)離屏渲染。為此我們采取了以下一些措施:

  • 即刻大量應(yīng)用AsyncDisplayKit(Texture)作為主要渲染框架,對(duì)于文字和圖片的異步渲染操作交由框架來處理。關(guān)于這方面可以看我之前的一些介紹
  • 對(duì)于圖片的圓角,統(tǒng)一采用“precomposite”的策略,也就是不經(jīng)由容器來做剪切,而是預(yù)先使用CoreGraphics為圖片裁剪圓角
  • 對(duì)于視頻的圓角,由于實(shí)時(shí)剪切非常消耗性能,我們會(huì)創(chuàng)建四個(gè)白色弧形的layer蓋住四個(gè)角,從視覺上制造圓角的效果
  • 對(duì)于view的圓形邊框,如果沒有backgroundColor,可以放心使用cornerRadius來做
  • 對(duì)于所有的陰影,使用shadowPath來規(guī)避離屏渲染
  • 對(duì)于特殊形狀的view,使用layer mask并打開shouldRasterize來對(duì)渲染結(jié)果進(jìn)行緩存
  • 對(duì)于模糊效果,不采用系統(tǒng)提供的UIVisualEffect,而是另外實(shí)現(xiàn)模糊效果(CIGaussianBlur),并手動(dòng)管理渲染結(jié)果
即刻客戶端中有大量的圓角、陰影等效果

總結(jié)

離屏渲染牽涉了很多Core Animation、GPU和圖形學(xué)等等方面的知識(shí),在實(shí)踐中也非??简?yàn)一個(gè)工程師排查問題的基本功、經(jīng)驗(yàn)和判斷能力——如果在不恰當(dāng)?shù)臅r(shí)候打開了shouldRasterize,只會(huì)弄巧成拙。

 

關(guān)注深藍(lán)互聯(lián)公眾號(hào)
Copyright ? 2013-2025 深藍(lán)互聯(lián) 版權(quán)所有
友情鏈接: