離屏渲染應(yīng)該是所有iOS開(kāi)發(fā)者繞不開(kāi)的話題,關(guān)于離屏渲染的文章也有很多。objc.io 的文章繪制像素到屏幕上說(shuō)過(guò):
一般情況下票腰,你需要避免離屏渲染,因?yàn)檫@是很大的消耗女气。直接將圖層合成到幀的緩沖區(qū)中(在屏幕上)比先創(chuàng)建屏幕外緩沖區(qū)杏慰,然后渲染到紋理中,最后將結(jié)果渲染到幀的緩沖區(qū)中要廉價(jià)很多炼鞠。因?yàn)檫@其中涉及兩次昂貴的環(huán)境轉(zhuǎn)換(轉(zhuǎn)換環(huán)境到屏幕外緩沖區(qū)缘滥,然后轉(zhuǎn)換環(huán)境到幀緩沖區(qū))。
之前不懂GPU的工作原理簇搅,不懂OpenGL/Metal這些底層繪制API完域,對(duì)這一段話理解的非常模糊,后來(lái)學(xué)了下OpenGL和Metal瘩将,再結(jié)合之前看的文章和自己的理解吟税,對(duì)離屏渲染(Offscreen Rendering)做一次梳理。有些地方是我自己的理解姿现,不見(jiàn)得正確肠仪。先拋出下面的問(wèn)題:
1、到底什么是離屏渲染备典?是在GPU上面還是CPU上面執(zhí)行的咪辱?
2饲嗽、為什么要有離屏渲染?什么情況下會(huì)產(chǎn)生離屏渲染?
2吮旅、幀緩沖區(qū)是什么?當(dāng)前屏幕緩沖區(qū)和屏幕外緩沖區(qū)又是什么被廓?
3党觅、切換緩沖區(qū)是什么操作?真的比較耗時(shí)嗎倚喂?
什么是離屏渲染?
2014年的WWDC Advanced Graphics and Animations for iOS Apps 對(duì)Core Animation的渲染機(jī)制做了詳細(xì)解釋?zhuān)?br>
現(xiàn)今移動(dòng)設(shè)備GPU都是采用Tile Based Rendering方式繪制每篷,關(guān)于Tile Based Rendering在這篇文章有詳細(xì)描述 Performance Tuning for
Tile-Based Architectures。
Core Animation打包圖層和動(dòng)畫(huà)信息到Render Server(這是一個(gè)單獨(dú)的進(jìn)程,所有app都會(huì)與這個(gè)進(jìn)程通信以完成最終的繪制)焦读,由Render Server調(diào)用OpenGL/Metal指令子库,最終在GPU上面完成繪制。而在GPU上面的工作對(duì)于了解OpenGL的人比較熟悉了矗晃,頂點(diǎn)著色器(Vertex Shader)和GPU tiling一起構(gòu)成Tiler操作仑嗅,然后通過(guò)片元著色器(Pixel Shader)做Renderer操作,最后輸出到渲染緩存(Render Buffer)中喧兄。這對(duì)應(yīng)一個(gè)渲染管線的完整流程无畔,實(shí)際上渲染管線還包括諸如光柵化,剔除吠冤,混合等操作浑彰,在上面的示意圖中省略了。對(duì)于普通的屏幕內(nèi)渲染拯辙,GPU只有一個(gè)Rendering Pass郭变。
對(duì)于離屏渲染,就存在多個(gè)Rendering Pass了涯保。上面是Masking操作的示意圖诉濒,一共有3步操作,對(duì)應(yīng)3個(gè)Rendering Pass夕春。最后的Compisiting pass輸出到最后的幀緩存未荒,是屏幕內(nèi)渲染,而前面的pass1和pass2是繪制到texture供最后一個(gè)pass所用及志,即離屏渲染片排。
離屏渲染在GPU上面執(zhí)行還是在CPU上面執(zhí)行?
前面的圖2很明確指出離屏渲染是在GPU上面執(zhí)行的,但是有很多文章說(shuō)CPU上面也會(huì)有離屏渲染速侈,比如使用Core Graphics繪制的時(shí)候率寡。Apple提供了檢測(cè)離屏渲染的工具:Color Off-screen Rendered
我重寫(xiě)UIView的drawRect方法(使用Core Graphics繪制),用Color Off-screen Rendered檢測(cè)(iOS12模擬器)沒(méi)有離屏渲染倚搬。因此嚴(yán)格來(lái)說(shuō)CPU渲染不應(yīng)該算作離屏渲染冶共,離屏渲染發(fā)生在GPU上面。而且CPU渲染導(dǎo)致的卡頓和GPU的離屏渲染導(dǎo)致的卡頓原理完全不一樣每界,在做性能優(yōu)化的時(shí)候應(yīng)該區(qū)別對(duì)待捅僵。
Core Graphics做繪制的時(shí)候,會(huì)有上下文Context眨层,也有一個(gè)Bitmap畫(huà)布命咐,但是這個(gè)Bitmap畫(huà)布是在CPU內(nèi)存上面的,上下文Context也和上面說(shuō)的環(huán)境轉(zhuǎn)換:昂貴的環(huán)境轉(zhuǎn)換(轉(zhuǎn)換環(huán)境到屏幕外緩沖區(qū)谐岁,然后轉(zhuǎn)換環(huán)境到幀緩沖區(qū))不是一碼概念。因此把CPU繪制歸為離屏渲染個(gè)人感覺(jué)非常不妥。
UIKit 早期成員 Andy Matuschak伊佃,在一篇回復(fù)中有這樣一段話:
In particular, a few (implementing drawRect and doing any CoreGraphics drawing, drawing with CoreText [which is just using CoreGraphics]) are indeed “offscreen drawing,” but they’re not what we usually mean when we say that. They’re very different from the rest of the list. When you implement drawRect or draw with CoreGraphics, you’re using the CPU to draw, and that drawing will happen synchronously within your application. You’re just calling some function which writes bits in a bitmap buffer, basically.
離屏渲染性能不好在哪里窜司?
Advanced Graphics and Animations for iOS Apps 這個(gè) session 以UIVisualEffectView為例描述GPU的處理邏輯,這里有五個(gè)Rendering Pass航揉,上面藍(lán)色為T(mén)iler操作的時(shí)間分布塞祈,紅色對(duì)應(yīng)Renderer操作。實(shí)際上GPU時(shí)間大部分都花在Renderer操作上面帅涂,同樣最后一個(gè)Rendering Pass是屏幕內(nèi)渲染议薪,那么UIVisualEffectView存在4個(gè)屏幕外的Rendering Pass。Rendering Pass之間還還存在黃色的Idle Time媳友,這個(gè)就是環(huán)境轉(zhuǎn)換(Context Switch)的時(shí)間斯议,一個(gè)Context Switch大概占用0.1ms-0.2ms的時(shí)間,那么UIVisualEffectView的所有Rendering Pass會(huì)累積0.5-1.0ms的Idle Time醇锚,這個(gè)在16.67ms的幀時(shí)間內(nèi)還是相當(dāng)大的哼御。因此離屏渲染性能不好在于:
1、更多的Rendering Pass焊唬,GPU運(yùn)算量增大恋昼;
2、Rendering Pass之間的Context Switch導(dǎo)致的Idle Time赶促。
為什么要有離屏渲染?
離屏渲染既然不好液肌,為什么它還存在?這要從OpenGL/Metal和GPU說(shuō)起鸥滨,GPU有少量的邏輯處理單元和大量的核心嗦哆,CPU則相反。CPU適合做邏輯運(yùn)算爵赵,復(fù)雜的運(yùn)算吝秕,而GPU適合做簡(jiǎn)單運(yùn)算,大量重復(fù)運(yùn)算空幻。對(duì)于Tiler中的大量頂點(diǎn)運(yùn)算烁峭,Renderer中的著色混合等都適合在GPU上面并行運(yùn)算。但是GPU不適合做邏輯運(yùn)算秕铛,所以一次只能繪制簡(jiǎn)單的圖元(Primitives)约郁,對(duì)應(yīng)到OpenGL中就是GL_POINTS、GL_LINES但两、GL_LINE_STRIP鬓梅、GL_TRIANGLES、GL_TRIANGLES_STRIP谨湘,比如一個(gè)CALayer就由兩個(gè)三角形(GL_TRIANGLES)組成绽快,繪制普通Layer的時(shí)候GPU只需要一個(gè)Render Pass即可芥丧。而mask效果是將一個(gè)層作為“形狀”來(lái)繪制另一個(gè)層,而這個(gè)“形狀”是無(wú)法通過(guò)點(diǎn)坊罢,線续担,三角形這些基本圖元來(lái)描述的,因此mask效果無(wú)法用GPU一步繪制出來(lái)活孩,但是可以多步組合繪制出來(lái)物遇,如上圖2描述的三個(gè)Rendering Pass組合繪制mask效果。
除此之外憾儒,我們知道CALayer的shouldRasterize屬性可以強(qiáng)制離屏渲染询兴。Advanced Graphics and Animations for iOS Apps 這樣描述:
Rasterization會(huì)使用GPU將多個(gè)Layer繪制到一個(gè)image(Texture)中,并且這個(gè)image是會(huì)緩存的起趾,以便后續(xù)直接使用緩存進(jìn)行渲染诗舰。在Rendering階段,存在一個(gè)顏色混合(https://learnopengl-cn.readthedocs.io/zh/latest/04%20Advanced%20OpenGL/03%20Blending/)的操作阳掐,即對(duì)應(yīng)到每一個(gè)像素點(diǎn)始衅,在繪制的時(shí)候都會(huì)取rendertBuffer中的原有顏色與當(dāng)前顏色按照指定公式計(jì)算得到顏色值。對(duì)應(yīng)到OpenGL就是:
glEnable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
這個(gè)操作在GPU上面是比較耗時(shí)的(因?yàn)樵谝苿?dòng)平臺(tái)GPU上缭保,取rendertBuffer中取顏色是消耗操作)汛闸。如果CALayer樹(shù)結(jié)構(gòu)比較復(fù)雜,圖層眾多艺骂,GPU每一幀都得混合所有的層诸老,導(dǎo)致GPU消耗巨大。Rasterization的作用在于可以指定用GPU混合一些復(fù)雜的CALayer成Texture钳恕,后續(xù)直接使用别伏,從而避免GPU的混合消耗∮嵌睿可見(jiàn)離屏渲染不一定降低性能厘肮,有時(shí)候還可以優(yōu)化性能。注意圖4指明Rasterization會(huì)增加內(nèi)存消耗睦番,同時(shí)只適合在圖層內(nèi)容變化不頻繁的場(chǎng)景类茂。
幀緩沖區(qū)是什么?
幀緩沖區(qū)(Frame Buffer)在OpenGL和Metal里面是最基礎(chǔ)的概念托嚣,可以理解為一塊內(nèi)存畫(huà)布巩检,類(lèi)似于Core Graphics的畫(huà)布一樣。對(duì)于屏幕內(nèi)渲染示启,會(huì)將畫(huà)布的內(nèi)容輸出到屏幕上兢哭,指定目標(biāo)為Render Buffer,在OpenGL中用glFramebufferRenderbuffer來(lái)指定:
glGenFramebuffers(1, &framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, sampleColorRenderbuffer);
glDrawArrays 或者 glDrawElements
對(duì)于離屏渲染夫嗓,畫(huà)布的內(nèi)容輸出到Texture上迟螺,使用glFramebufferTexture2D指定冲秽,比如:
// rendering pass1
glGenFramebuffers(1, &framebuffer1);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer1);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture1, 0);
glDrawArrays 或者 glDrawElements
// rendering pass2
glGenFramebuffers(1, &framebuffer2);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer2);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture2, 0);
glDrawArrays 或者 glDrawElements
...
// final rendering pass
glBindFramebuffer(GL_FRAMEBUFFER, framebufferFinal);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, sampleColorRenderbuffer);
//
glGenFramebuffers為開(kāi)辟幀緩沖區(qū),glBindFramebuffer為切換幀緩沖區(qū)煮仇,對(duì)于離屏渲染會(huì)有更多的內(nèi)存分配和切換操作劳跃。從上面分析離屏消耗消耗主要在于:
1、glGenFramebuffers導(dǎo)致更多的內(nèi)存消耗浙垫;
2、更多的glDrawArrays和glDrawElements導(dǎo)致GPU有更多繪制操作郑诺;
3夹姥、切換緩沖區(qū)帶來(lái)的消耗:實(shí)際上glBindFramebuffer命令并不會(huì)有過(guò)多的消耗,根據(jù)Andy Matuschak辙诞,在回復(fù)中的原話:It’s expensive for the GPU to switch contexts from on-screen to off-screen drawing (it must flush its pipelines and barrier)辙售,意思是離屏渲染的Context切換會(huì)導(dǎo)致渲染管線的flush操作,眾所周知OpenGL中的glFlush操作是非常昂貴的飞涂,所以我理解這才是Context切換昂貴的最終原因旦部。
為什么Context切換會(huì)導(dǎo)致flush
先從表面看圖2,mask效果需要3個(gè)renderIng pass较店,只有最后一個(gè)rendering pass是輸出到屏幕顯示士八,而前面兩個(gè)rendering pass都是渲染出Texture作為最后一個(gè)的輸入,而最后一個(gè)rendering pass要想獲得正確的結(jié)果梁呈,前面的rendering pass必須先完成婚度。GPU是多核并行計(jì)算,而這種依賴(lài)關(guān)系導(dǎo)致rendering pass無(wú)法真正并行執(zhí)行官卡。
實(shí)際上蝗茁,只要是將Frame Buffer渲染到Texture,Texture又用于后續(xù)的渲染寻咒,那么后續(xù)的渲染都會(huì)等待前面的Texture渲染完成哮翘,即glFlush操作,以保證最終結(jié)果的正確毛秘。Performance Tuning for
Tile-Based Architectures這篇文章也指明使用render to texture的結(jié)果會(huì)導(dǎo)致flush饭寺。
參考
https://objccn.io/issue-3-1/
https://www.seas.upenn.edu/~pcozzi/OpenGLInsights/OpenGLInsights-TileBasedArchitectures.pdf
https://github.com/seedante/iOS-Note/wiki/Mastering-Offscreen-Render
https://developer.apple.com/videos/play/wwdc2014/419/
https://learnopengl-cn.readthedocs.io/zh/latest/04%20Advanced%20OpenGL/03%20Blending/