[轉(zhuǎn)]關(guān)于iOS離屏渲染的深入研究

關(guān)于iOS離屏渲染的深入研究

在平時的iOS面試中奔穿,我們經(jīng)常會考察有關(guān)離屏渲染(Offscreen rendering)的知識點闻鉴。一般來說,絕大多數(shù)人都能答出“圓角芒篷、mask搜变、陰影會觸發(fā)離屏渲染”针炉,但是也僅止于此挠他。如果再問得深入哪怕一點點糊识,比如:

  • 離屏渲染是在哪一步進行的赂苗?為什么愉耙?
  • 設(shè)置cornerRadius一定會觸發(fā)離屏渲染嗎?

90%的候選人都沒法非常確定地說出答案拌滋。作為一個客戶端工程師朴沿,把控渲染性能是最關(guān)鍵、最獨到的技術(shù)要點之一败砂,如果僅僅了解表面知識赌渣,到了實際應(yīng)用時往往會失之毫厘謬以千里,無法得到預期的效果昌犹。

iOS渲染架構(gòu)

在WWDC的Advanced Graphics and Animations for iOS Apps(WWDC14 419坚芜,關(guān)于UIKit和Core Animation基礎(chǔ)的session在早年的WWDC中比較多)中有這樣一張圖:

image

我們可以看到,在Application這一層中主要是CPU在操作斜姥,而到了Render Server這一層鸿竖,CoreAnimation會將具體操作轉(zhuǎn)換成發(fā)送給GPU的draw calls(以前是call OpenGL ES沧竟,現(xiàn)在慢慢轉(zhuǎn)到了Metal),顯然CPU和GPU雙方同處于一個流水線中缚忧,協(xié)作完成整個渲染工作悟泵。

離屏渲染的定義

如果要在顯示屏上顯示內(nèi)容,我們至少需要一塊與屏幕像素數(shù)據(jù)量一樣大的frame buffer闪水,作為像素數(shù)據(jù)存儲區(qū)域糕非,而這也是GPU存儲渲染結(jié)果的地方。如果有時因為面臨一些限制球榆,無法把渲染結(jié)果直接寫入frame buffer朽肥,而是先暫存在另外的內(nèi)存區(qū)域,之后再寫入frame buffer芜果,那么這個過程被稱之為離屏渲染鞠呈。

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

CPU”離屏渲染“

大家知道右钾,如果我們在UIView中實現(xiàn)了drawRect方法蚁吝,就算它的函數(shù)體內(nèi)部實際沒有代碼,系統(tǒng)也會為這個view申請一塊內(nèi)存區(qū)域舀射,等待CoreGraphics可能的繪畫操作窘茁。

對于類似這種“新開一塊CGContext來畫圖“的操作,有很多文章和視頻也稱之為“離屏渲染”(因為像素數(shù)據(jù)是暫時存入了CGContext脆烟,而不是直接到了frame buffer)山林。進一步來說,其實所有CPU進行的光柵化操作(如文字渲染邢羔、圖片解碼)驼抹,都無法直接繪制到由GPU掌管的frame buffer,只能暫時先放在另一塊內(nèi)存之中拜鹤,說起來都屬于“離屏渲染”框冀。

自然我們會認為,因為CPU不擅長做這件事敏簿,所以我們需要盡量避免它明也,就誤以為這就是需要避免離屏渲染的原因。但是根據(jù)蘋果工程師的說法惯裕,CPU渲染并非真正意義上的離屏渲染温数。另一個證據(jù)是,如果你的view實現(xiàn)了drawRect蜻势,此時打開Xcode調(diào)試的“Color offscreen rendered yellow”開關(guān)撑刺,你會發(fā)現(xiàn)這片區(qū)域不會被標記為黃色,說明Xcode并不認為這屬于離屏渲染握玛。

其實通過CPU渲染就是俗稱的“軟件渲染”猜煮,而真正的離屏渲染發(fā)生在GPU次员。

GPU離屏渲染

在上面的渲染流水線示意圖中我們可以看到,主要的渲染操作都是由CoreAnimation的Render Server模塊王带,通過調(diào)用顯卡驅(qū)動所提供的OpenGL/Metal接口來執(zhí)行的。通常對于每一層layer市殷,Render Server會遵循“畫家算法”愕撰,按次序輸出到frame buffer,后一層覆蓋前一層醋寝,就能得到最終的顯示結(jié)果(值得一提的是搞挣,與一般桌面架構(gòu)不同,在iOS中音羞,設(shè)備主存和GPU的顯存共享物理內(nèi)存囱桨,這樣可以省去一些數(shù)據(jù)傳輸開銷)。

”畫家算法“嗅绰,把每一層依次輸出到畫布

然而有些場景并沒有那么簡單舍肠。作為“畫家”的GPU雖然可以一層一層往畫布上進行輸出,但是無法在某一層渲染完成之后窘面,再回過頭來擦除/改變其中的某個部分——因為在這一層之前的若干層layer像素數(shù)據(jù)翠语,已經(jīng)在渲染中被永久覆蓋了。這就意味著财边,對于每一層layer肌括,要么能找到一種通過單次遍歷就能完成渲染的算法,要么就不得不另開一塊內(nèi)存酣难,借助這個臨時中轉(zhuǎn)區(qū)域來完成一些更復雜的谍夭、多次的修改/剪裁操作

如果要繪制一個帶有圓角并剪切圓角以外內(nèi)容的容器憨募,就會觸發(fā)離屏渲染紧索。我的猜想是(如果讀者中有圖形學專家希望能指正):

  • 將一個layer的內(nèi)容裁剪成圓角,可能不存在一次遍歷就能完成的方法
  • 容器的子layer因為父容器有圓角馋嗜,那么也會需要被裁剪齐板,而這時它們還在渲染隊列中排隊,尚未被組合到一塊畫布上葛菇,自然也無法統(tǒng)一裁剪

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

常見離屏渲染場景分析

  • cornerRadius+clipsToBounds莺债,原因就如同上面提到的滋觉,不得已只能另開一塊內(nèi)存來操作签夭。而如果只是設(shè)置cornerRadius(如不需要剪切內(nèi)容,只需要一個帶圓角的邊框)椎侠,或者只是需要裁掉矩形區(qū)域以外的內(nèi)容(雖然也是剪切第租,但是稍微想一下就可以發(fā)現(xiàn),對于純矩形而言我纪,實現(xiàn)這個算法似乎并不需要另開內(nèi)存)慎宾,并不會觸發(fā)離屏渲染。關(guān)于剪切圓角的性能優(yōu)化浅悉,根據(jù)場景不同有幾個方案可供選擇趟据,非常推薦閱讀AsyncDisplayKit中的一篇文檔
ASDK中對于如何選擇圓角渲染策略的流程圖术健,非常實用
  • shadow汹碱,其原因在于,雖然layer本身是一塊矩形區(qū)域荞估,但是陰影默認是作用在其中”非透明區(qū)域“的咳促,而且需要顯示在所有l(wèi)ayer內(nèi)容的下方,因此根據(jù)畫家算法必須被渲染在先泼舱。但矛盾在于此時陰影的本體(layer和其子layer)都還沒有被組合到一起等缀,怎么可能在第一步就畫出只有完成最后一步之后才能知道的形狀呢?這樣一來又只能另外申請一塊內(nèi)存娇昙,把本體內(nèi)容都先畫好尺迂,再根據(jù)渲染結(jié)果的形狀,添加陰影到frame buffer冒掌,最后把內(nèi)容畫上去(這只是我的猜測噪裕,實際情況可能更復雜)。不過如果我們能夠預先告訴CoreAnimation(通過shadowPath屬性)陰影的幾何形狀股毫,那么陰影當然可以先被獨立渲染出來膳音,不需要依賴layer本體,也就不再需要離屏渲染了铃诬。

[圖片上傳中...(image-b1d1e1-1563519618469-7)]

<figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">陰影會作用在所有子layer所組成的形狀上祭陷,那就只能等全部子layer畫完才能得到</figcaption>

  • group opacity,其實從名字就可以猜到趣席,alpha并不是分別應(yīng)用在每一層之上兵志,而是只有到整個layer樹畫完之后,再統(tǒng)一加上alpha宣肚,最后和底下其他layer的像素進行組合想罕。顯然也無法通過一次遍歷就得到最終結(jié)果。將一對藍色和紅色layer疊在一起霉涨,然后在父layer上設(shè)置opacity=0.5按价,并復制一份在旁邊作對比惭适。左邊關(guān)閉group opacity,右邊保持默認(從iOS7開始楼镐,如果沒有顯式指定癞志,group opacity會默認打開),然后打開offscreen rendering的調(diào)試框产,我們會發(fā)現(xiàn)右邊的那一組確實是離屏渲染了今阳。
同樣的兩個view,右邊打開group opacity(默認行為)的被標記為Offscreen rendering
  • mask茅信,我們知道m(xù)ask是應(yīng)用在layer和其所有子layer的組合之上的,而且可能帶有透明度墓臭,那么其實和group opacity的原理類似蘸鲸,不得不在離屏渲染中完成。
WWDC中蘋果的解釋窿锉,mask需要遍歷至少三次
  • UIBlurEffect酌摇,同樣無法通過一次遍歷完成,其原理在WWDC中提到:

[圖片上傳中...(image-e2ea6-1563519618469-4)]

  • 其他還有一些嗡载,類似allowsEdgeAntialiasing等等也可能會觸發(fā)離屏渲染窑多,原理也都是類似:如果你無法僅僅使用frame buffer來畫出最終結(jié)果,那就只能另開一塊內(nèi)存空間來儲存中間結(jié)果洼滚。這些原理并不神秘埂息。

GPU離屏渲染的性能影響

GPU的操作是高度流水線化的。本來所有計算工作都在有條不紊地正在向frame buffer輸出遥巴,此時突然收到指令千康,需要輸出到另一塊內(nèi)存,那么流水線中正在進行的一切都不得不被丟棄铲掐,切換到只能服務(wù)于我們當前的“切圓角”操作拾弃。等到完成以后再次清空,再回到向frame buffer輸出的正常流程摆霉。

在tableView或者collectionView中豪椿,滾動的每一幀變化都會觸發(fā)每個cell的重新繪制,因此一旦存在離屏渲染携栋,上面提到的上下文切換就會每秒發(fā)生60次搭盾,并且很可能每一幀有幾十張的圖片要求這么做,對于GPU的性能沖擊可想而知(GPU非常擅長大規(guī)模并行計算刻两,但是我想頻繁的上下文切換顯然不在其設(shè)計考量之中)

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

善用離屏渲染

盡管離屏渲染開銷很大,但是當我們無法避免它的時候磅摹,可以想辦法把性能影響降到最低滋迈。優(yōu)化思路也很簡單:既然已經(jīng)花了不少精力把圖片裁出了圓角霎奢,如果我能把結(jié)果緩存下來,那么下一幀渲染就可以復用這個成果饼灿,不需要再重新畫一遍了幕侠。

CALayer為這個方案提供了對應(yīng)的解法:shouldRasterize。一旦被設(shè)置為true碍彭,Render Server就會強制把layer的渲染結(jié)果(包括其子layer晤硕,以及圓角、陰影庇忌、group opacity等等)保存在一塊內(nèi)存中舞箍,這樣一來在下一幀仍然可以被復用,而不會再次觸發(fā)離屏渲染皆疹。有幾個需要注意的點:

  • shouldRasterize的主旨在于降低性能損失疏橄,但總是至少會觸發(fā)一次離屏渲染。如果你的layer本來并不復雜略就,也沒有圓角陰影等等捎迫,打開這個開關(guān)反而會增加一次不必要的離屏渲染
  • 離屏渲染緩存有空間上限,最多不超過屏幕總像素的2.5倍大小
  • 一旦緩存超過100ms沒有被使用表牢,會自動被丟棄
  • layer的內(nèi)容(包括子layer)必須是靜態(tài)的窄绒,因為一旦發(fā)生變化(如resize,動畫)崔兴,之前辛苦處理得到的緩存就失效了彰导。如果這件事頻繁發(fā)生,我們就又回到了“每一幀都需要離屏渲染”的情景恼布,而這正是開發(fā)者需要極力避免的螺戳。針對這種情況,Xcode提供了“Color Hits Green and Misses Red”的選項折汞,幫助我們查看緩存的使用是否符合預期
  • 其實除了解決多次離屏渲染的開銷倔幼,shouldRasterize在另一個場景中也可以使用:如果layer的子結(jié)構(gòu)非常復雜,渲染一次所需時間較長爽待,同樣可以打開這個開關(guān)损同,把layer繪制到一塊緩存,然后在接下來復用這個結(jié)果鸟款,這樣就不需要每次都重新繪制整個layer樹了

什么時候需要CPU渲染

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

平衡CPU和GPU的負載

絕大多數(shù)情況下组哩,得益于GPU針對圖形處理的優(yōu)化,我們都會傾向于讓GPU來完成渲染任務(wù),而給CPU留出足夠時間處理各種各樣復雜的App邏輯伶贰。為此Core Animation做了大量的工作蛛砰,盡量把渲染工作轉(zhuǎn)換成適合GPU處理的形式(也就是所謂的硬件加速,如layer composition黍衙,設(shè)置backgroundColor等等)泥畅。

但是對于一些情況,如文字(CoreText使用CoreGraphics渲染)和圖片(ImageIO)渲染琅翻,由于GPU并不擅長做這些工作位仁,不得不先由CPU來處理好以后,再把結(jié)果作為texture傳給GPU方椎。除此以外聂抢,有時候也會遇到GPU實在忙不過來的情況,而CPU相對空閑(GPU瓶頸)棠众,這時可以讓CPU分擔一部分工作涛浙,提高整體效率。

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

一個典型的例子是,我們經(jīng)常會使用CoreGraphics給圖片加上圓角(將圖片中圓角以外的部分渲染成透明)疮薇。整個過程全部是由CPU完成的胸墙。這樣一來既然我們已經(jīng)得到了想要的效果,就不需要再另外給圖片容器設(shè)置cornerRadius按咒。另一個好處是迟隅,我們可以靈活地控制裁剪和緩存的時機,巧妙避開CPU和GPU最繁忙的時段励七,達到平滑性能波動的目的智袭。

這里有幾個需要注意的點:

  • 渲染不是CPU的強項,調(diào)用CoreGraphics會消耗其相當一部分計算時間掠抬,并且我們也不愿意因此阻塞用戶操作吼野,因此一般來說CPU渲染都在后臺線程完成(這也是AsyncDisplayKit的主要思想),然后再回到主線程上两波,把渲染結(jié)果傳回CoreAnimation瞳步。這樣一來,多線程間數(shù)據(jù)同步會增加一定的復雜度
  • 同樣因為CPU渲染速度不夠快腰奋,因此只適合渲染靜態(tài)的元素单起,如文字、圖片(想象一下沒有硬件加速的視頻解碼劣坊,性能慘不忍睹)
  • 作為渲染結(jié)果的bitmap數(shù)據(jù)量較大(形式上一般為解碼后的UIImage)嘀倒,消耗內(nèi)存較多,所以應(yīng)該在使用完及時釋放,并在需要的時候重新生成测蘑,否則很容易導致OOM
  • 如果你選擇使用CPU來做渲染灌危,那么就沒有理由再觸發(fā)GPU的離屏渲染了,否則會同時存在兩塊內(nèi)容相同的內(nèi)存帮寻,而且CPU和GPU都會比較辛苦
  • 一定要使用Instruments的不同工具來測試性能乍狐,而不是僅憑猜測來做決定

即刻的優(yōu)化

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

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

總結(jié)

離屏渲染牽涉了很多Core Animation嘶伟、GPU和圖形學等等方面的知識怎憋,在實踐中也非常考驗一個工程師排查問題的基本功九昧、經(jīng)驗和判斷能力——如果在不恰當?shù)臅r候打開了shouldRasterize盛霎,只會弄巧成拙。

從一個更廣闊的視角看耽装,離屏渲染也僅僅是渲染性能優(yōu)化中的一部分愤炸,而能否保證UI性能過關(guān),將會直接影響到用戶日常的操作體驗掉奄。渲染技術(shù)作為客戶端工程師的關(guān)鍵技術(shù)能力之一规个,值得持續(xù)研究凤薛。如果你對這方面也有追求,可以發(fā)送簡歷到 jasy@ruguoapp.com诞仓,歡迎加入我們缤苫!

推薦資料

Andy Matuschak關(guān)于離屏渲染的解釋

Objc.io: Moving Pixels onto the Screen

Mastering Offscreen Render

WWDC 2011 421 Core Animation Essentials

WWDC 2011 121 Understanding UIKit Rendering

WWDC 2014 419 Advanced Graphics and Animations for iOS Apps

WWDC 2010 135 Advanced Performance Optimization on iPhone OS Part 1

《Core Animation: Advanced Techniques》

原文鏈接:https://zhuanlan.zhihu.com/p/72653360

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市墅拭,隨后出現(xiàn)的幾起案子活玲,更是在濱河造成了極大的恐慌,老刑警劉巖谍婉,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件舒憾,死亡現(xiàn)場離奇詭異,居然都是意外死亡穗熬,警方通過查閱死者的電腦和手機镀迂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來唤蔗,“玉大人探遵,你說我怎么就攤上這事〖斯瘢” “怎么了箱季?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長棍掐。 經(jīng)常有香客問我规哪,道長,這世上最難降的妖魔是什么塌衰? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮蝠嘉,結(jié)果婚禮上最疆,老公的妹妹穿的比我還像新娘。我一直安慰自己蚤告,他們只是感情好努酸,可當我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著杜恰,像睡著了一般获诈。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上心褐,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天舔涎,我揣著相機與錄音,去河邊找鬼逗爹。 笑死亡嫌,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播挟冠,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼于购,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了知染?” 一聲冷哼從身側(cè)響起肋僧,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎控淡,沒想到半個月后嫌吠,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡逸寓,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年居兆,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片竹伸。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡泥栖,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出勋篓,到底是詐尸還是另有隱情吧享,我是刑警寧澤,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布譬嚣,位于F島的核電站钢颂,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏拜银。R本人自食惡果不足惜殊鞭,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望尼桶。 院中可真熱鬧操灿,春花似錦、人聲如沸泵督。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽小腊。三九已至救鲤,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間秩冈,已是汗流浹背本缠。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留入问,地道東北人搓茬。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓犹赖,卻偏偏與公主長得像,于是被迫代替她去往敵國和親卷仑。 傳聞我的和親對象是個殘疾皇子峻村,可洞房花燭夜當晚...
    茶點故事閱讀 42,792評論 2 345