iOS 離屏渲染分析/優(yōu)化

開始前的提問:
1.離屏渲染是什么秆撮?
2.離屏渲染在哪一步進(jìn)行的四濒?
3.離屏渲染的影響在哪?
4.設(shè)置圓角一定會(huì)觸發(fā)離屏渲染嗎职辨?
5.如何優(yōu)化離屏渲染盗蟆?

深入理解了上面幾個(gè)問題足以回答面試官的問題。

iOS中圖像渲染流程

UIKit其實(shí)就是CoreGraphicsCoreAnimation的高度集成舒裤。
我們通過UIKit實(shí)現(xiàn)可視化的控件布局其實(shí)就是CoreGraphics繪制圖層喳资,但是它顯示的部分和它的動(dòng)畫其實(shí)是CoreAnimation來完成。

CoreGraphics它用來處理在運(yùn)行前創(chuàng)建的圖像腾供。比如說在工程中導(dǎo)入的圖片/資源文件仆邓。它可以對(duì)現(xiàn)成的文件進(jìn)行高效的處理,既可以在CPU也可以在GPU上執(zhí)行

CoreAnimation用來做核心動(dòng)畫伴鳖,圖形圖像顯示节值。
CoreImage做一些濾鏡處理操作

圖像渲染框架.png

在Application階段,用CPU處理黎侈。CPU創(chuàng)建視圖察署;計(jì)算視圖的frame;進(jìn)行圖片解碼峻汉、繪制紋理等等贴汪。再交由我們的GPU。
再由頂點(diǎn)著色器去確定圖形在我們硬件上的具體顯示位置休吠。
進(jìn)行圖元裝配扳埂,光柵化。
由片元著色器去確定計(jì)算每一個(gè)像素點(diǎn)的顏色值瘤礁。
會(huì)找到對(duì)應(yīng)像素點(diǎn)的范圍阳懂,把每一個(gè)像素點(diǎn)的顏色顯示上去,然后再放入我們的幀緩存區(qū)里frameBuffer柜思。
最后由顯示系統(tǒng)將幀緩存區(qū)里的數(shù)據(jù)顯示出來岩调。

圖像渲染流程..png
渲染路線.png

視頻控制器是怎么把幀緩存區(qū)的數(shù)據(jù)顯示出來的?
通過逐行掃描的方式確定一幀畫面的顯示赡盘。再回到初始的點(diǎn)逐行掃描確定下一幀的圖像号枕。

我們的顯示器通常都是固定的形式刷新的。像我們蘋果手機(jī)刷新頻率每秒60Hz陨享。我們做屏幕優(yōu)化的時(shí)候都是以fps作為指標(biāo)葱淳。那么它的值越接近60說明屏幕流暢度越好

image.png

當(dāng)VSync垂直同步信號(hào)的時(shí)候,GPU它還沒有把這一幀的數(shù)據(jù)放入frameBuffer抛姑,我們這一個(gè)畫面幀就會(huì)被丟失赞厕。等待下一個(gè)垂直同步信號(hào)過來的時(shí)候,再來顯示我們前面的內(nèi)容定硝。

丟幀原因.png

離屏渲染是什么皿桑?

如果要在顯示屏上顯示內(nèi)容,我們至少需要一塊與屏幕像素?cái)?shù)據(jù)量一樣大的frameBuffer(幀緩存區(qū))作為像素?cái)?shù)據(jù)存儲(chǔ)區(qū)域蔬啡,然后由顯示器把幀緩存區(qū)的數(shù)據(jù)顯示到屏幕上唁毒。
如果有時(shí)因?yàn)槊媾R一些限制,比如說陰影/遮罩等等星爪,GPU無法吧渲染結(jié)果直接寫入frameBuffer浆西,而是先暫時(shí)把中間的一個(gè)臨時(shí)狀態(tài)存入另外新開辟的內(nèi)存區(qū)域,之后再寫入frameBuffer顽腾,這個(gè)過程被稱之為離屏渲染近零。

離屏渲染產(chǎn)生.png

當(dāng)視圖層級(jí)結(jié)構(gòu)比較復(fù)雜的時(shí)候,就需要開辟臨時(shí)內(nèi)存區(qū)域抄肖。

離屏渲染會(huì)有什么影響呢久信?

GPU計(jì)算出的復(fù)雜的渲染圖層,它會(huì)開辟一個(gè)臨時(shí)內(nèi)存區(qū)域漓摩;
離屏渲染的整個(gè)過程裙士,需要多次切換上下文環(huán)境:先是從當(dāng)前屏幕(On-Screen)切換到離屏(Off-Screen),等到離屏渲染結(jié)束以后管毙,將離屏緩沖區(qū)的渲染結(jié)果顯示到屏幕上又需要將上下文環(huán)境從離屏切換到當(dāng)前屏幕腿椎。而上下文環(huán)境的切換是要付出很大代價(jià)的桌硫。
由于垂直同步的機(jī)制,如果在一個(gè) HSync 時(shí)間內(nèi)啃炸,CPU 或者 GPU 沒有完成內(nèi)容提交铆隘,則那一幀就會(huì)被丟棄,等待下一次機(jī)會(huì)再顯示南用,而這時(shí)顯示屏?xí)A糁暗膬?nèi)容不變膀钠。這就是界面卡頓的原因。

既然離屏渲染這么耗性能,為什么有這套機(jī)制呢?

有些效果被認(rèn)為不能直接呈現(xiàn)于屏幕裹虫,而需要在別的地方做額外的處理預(yù)合成肿嘲。圖層屬性的混合體沒有預(yù)合成之前不能直接在屏幕中繪制,所以就需要屏幕外渲染筑公。屏幕外渲染并不意味著軟件繪制雳窟,但是它意味著圖層必須在被顯示之前在一個(gè)屏幕外上下文中被渲染(不論CPU還是GPU)。

CALayer產(chǎn)生GPU離屏渲染的操作與相應(yīng)的優(yōu)化手段

UIViewCALayer關(guān)系:
UIView繼承自UIResponder十酣,可以處理系統(tǒng)傳遞過來的事件涩拙,如:UIApplication、UIViewController耸采、UIView兴泥,以及所有從UIView派生出來的UIKit類。每個(gè)UIView內(nèi)部都有一個(gè)CALayer提供內(nèi)容的繪制和顯示虾宇,并且作為內(nèi)部RootLayer的代理視圖搓彻。

CALayer繼承自NSObject類,負(fù)責(zé)顯示UIView提供的內(nèi)容contents嘱朽。CALayer有三個(gè)視覺元素:背景色backgroundColor旭贬、內(nèi)容contents、邊緣borderWidth&borderColor構(gòu)成搪泳,其中稀轨,內(nèi)容的本質(zhì)是一個(gè)CGImage。

CALayer結(jié)構(gòu).png

以下離屏渲染操作岸军,按對(duì)性能影響等級(jí)從高到低進(jìn)行排序: shadows(陰影)奋刽、圓角mask(遮罩)艰赞、allowsGroupOpacity(組不透明)佣谐、edge antialiasing(抗鋸齒)

離屏渲染分析

如何查看渲染出來的UI是否經(jīng)過離屏渲染過程呢?
打開模擬器工具欄 -> Debug -> Color Off-screen Rendered
圖層變化成黃色方妖,說明是經(jīng)過離屏渲染狭魂。

模擬器.png

代碼部分很簡單,新建一個(gè)工程寫一個(gè)tableView,在tableView上面添加一個(gè)ImageView和一個(gè)UILabel即可雌澄。接下來逐個(gè)分析出現(xiàn)離屏渲染的情況斋泄。

Instruments使用

利用Core Animation來分析應(yīng)用的性能問題

Instruments.png

Color Blended Layers
這個(gè)選項(xiàng)基于渲染程度對(duì)屏幕中的混合區(qū)域進(jìn)行綠到紅的高亮(也就是多個(gè)半透明圖層的疊加)。由于重繪的原因掷伙,混合對(duì)GPU性能會(huì)有影響是己,同時(shí)也是滑動(dòng)或者動(dòng)畫幀率下降的罪魁禍?zhǔn)字?/p>

Color Hits Green and Misses Red
當(dāng)設(shè)置shouldRasterizep屬性為YES的時(shí)候又兵,耗時(shí)的圖層繪制會(huì)被緩存任柜,然后當(dāng)做一個(gè)簡單的扁平圖片呈現(xiàn)。當(dāng)緩存再生的時(shí)候這個(gè)選項(xiàng)就用紅色對(duì)柵格化圖層進(jìn)行了高亮沛厨。如果緩存頻繁再生的話宙地,就意味著柵格化可能會(huì)有負(fù)面的性能影響了

Color Offscreen-Rendered Yellow
開啟后會(huì)把那些需要離屏渲染的圖層高亮成黃色,這就意味著黃色圖層可能存在性能問題

當(dāng)然Debug還有其它的選項(xiàng)逆皮,來分析不同的性能問題宅粥,如有需求,請(qǐng)參考其它資料电谣。

光柵 - 觸發(fā)離屏渲染
    private func wj_shouldRasterize() {
        // 緩存機(jī)制 -- bitmap -- cpu直接從緩存取數(shù)據(jù) -- gpu渲染 -- 可提供性能 -- 緩存在100ms內(nèi)  慎用;嗝贰!剿牺!
        // 面試盡量別提光柵化企垦。若當(dāng)視圖內(nèi)容是動(dòng)態(tài)變化(如后臺(tái)下載圖片完畢后切換到主線程設(shè)置)時(shí),使用此方案反而為增加系統(tǒng)負(fù)荷晒来。
        imageV.layer.shouldRasterize = true
        imageV.layer.rasterizationScale = imageV.layer.contentsScale
    }
image.png
遮罩Mask - 觸發(fā)離屏渲染
    private func wj_mask() {
        // mask是添加在imageV.layer的上層
        let layer = CALayer()
        layer.frame = CGRect(x: 0, y: 0, width: imageV.bounds.size.width, height: imageV.bounds.size.height)
        layer.backgroundColor = UIColor.red.cgColor
        imageV.layer.mask = layer
    }
陰影 - 觸發(fā)離屏渲染
    private func wj_shadows() {
        // shadow是在imageV.layer的下層
        imageV.layer.shadowColor = UIColor.red.cgColor
        imageV.layer.shadowOpacity = 0.1
        imageV.layer.shadowRadius = 5
        imageV.layer.shadowOffset = CGSize(width: 10, height: 10)
    }
陰影優(yōu)化 - 不會(huì)離屏渲染
    private func wj_shadowsOptimize() {
        imageV.layer.shadowColor = UIColor.red.cgColor
        imageV.layer.shadowOpacity = 0.1
        imageV.layer.shadowRadius = 5
        // CoreAnimation - 陰影的幾何形狀
        imageV.layer.shadowPath = UIBezierPath(rect: CGRect(x: 0, y: 0, width: imageV.bounds.size.width+10, height: imageV.bounds.size.height+10)).cgPath // 10是陰影偏移量
    }
抗鋸齒 - 觸發(fā)離屏渲染
    private func wj_edgeAntialiasing() {
        let angel = CGFloat.pi/20.0
        imageV.layer.transform = CATransform3DRotate(imageV.layer.transform, angel, 0, 0, 1);
        imageV.clipsToBounds = true
        imageV.layer.allowsEdgeAntialiasing = true
    }
視圖組不透明 - 觸發(fā)離屏渲染
// 視圖組不透明 alpha 只要有子視圖并設(shè)置父視圖的alpha<1就會(huì)離屏渲染
    private func wj_allowsGroupOpacity() {
        // 沒有子視圖view是不會(huì)離屏渲染
        let view = UIView(frame: CGRect(x: 10, y: 10, width: 20, height: 20))
        view.backgroundColor = .green
        imageV.addSubview(view)
        
        imageV.alpha = 0.5
        imageV.layer.allowsGroupOpacity = true // 設(shè)置view與imageV透明度一樣
    }
圓角

圓角就一定會(huì)觸發(fā)離屏渲染嗎?
先告訴你答案是否定的湃崩!重點(diǎn)看分析:

圓角案例一
    // 不會(huì)觸發(fā)離屏渲染
    private func wj_radius() {
        imageV.layer.cornerRadius = 20
        imageV.clipsToBounds = true
    }
案例一不會(huì)觸發(fā).png
圓角案例二
    // 四個(gè)角就會(huì)離屏渲染
    // 設(shè)置borderWidth攒读、borderColor只要Color不為clear朵诫,四個(gè)角就會(huì)離屏渲染
    private func wj_radius() {
        imageV.layer.borderWidth = 1
        imageV.layer.borderColor = UIColor.red.cgColor
        imageV.layer.cornerRadius = 20
        imageV.clipsToBounds = true
    }
圓角案例二觸發(fā).png
圓角案例三
    // 添加子視圖 - 四個(gè)角就會(huì)離屏渲染
    private func wj_radius() {
        let view = UIView(frame: CGRect(x: 30, y: 30, width: 30, height: 30))
        view.backgroundColor = .green
        imageV.addSubview(view)
        imageV.layer.cornerRadius = 20
        imageV.clipsToBounds = true
    }
圓角案例三觸發(fā).png
圓角案例四:重點(diǎn)分析
image.png
    private func wj_radius() {
         // 加了背景顏色會(huì)觸發(fā)離屏渲染, 其實(shí)設(shè)置layer的backgroundColor
        imageV.backgroundColor = .red
        imageV.layer.cornerRadius = 20
        imageV.clipsToBounds = true

新增一行代碼給imageV添加背景色剪返,它就會(huì)觸發(fā)離屏渲染。
因?yàn)?code>imageV.backgroundColor其實(shí)設(shè)置的是layerbackgroundColor泌辫。

此時(shí)再添加一行代碼随夸,將image設(shè)置為nil震放,它就不會(huì)觸發(fā)離屏渲染了:

    private func wj_radius() {
         // 加了背景顏色會(huì)觸發(fā)離屏渲染, 其實(shí)設(shè)置layer的backgroundColor
        imageV.backgroundColor = .red
         // 實(shí)際是把contents層內(nèi)容設(shè)置為nil
        imageV.image = nil
        imageV.layer.cornerRadius = 20
        imageV.clipsToBounds = true
    }
image.png

這是因?yàn)?imageV.image = nil 實(shí)際上是把imageVcontents層內(nèi)容設(shè)置為nil殿遂。

此時(shí)再添加一行代碼設(shè)置layer的背景色

    private func wj_radius() {
         // 加了背景顏色會(huì)觸發(fā)離屏渲染, 其實(shí)設(shè)置layer的backgroundColor
        imageV.backgroundColor = .red
         // 實(shí)際是把contents層內(nèi)容設(shè)置為nil
        imageV.image = nil
        // 實(shí)際是contents層的背景設(shè)置為藍(lán)色
        imageV.layer.backgroundColor = UIColor.blue.cgColor
        imageV.layer.cornerRadius = 20
        imageV.clipsToBounds = true
    }
image.png

這就驗(yàn)證了上面圖層的結(jié)構(gòu)了,contents在layer的上層幢竹。

特殊的Label

上面嘗試僅僅給imageV添加背景色做圓角處理焕毫,它會(huì)觸發(fā)離屏渲染,而對(duì)于label它是不會(huì)觸發(fā)離屏渲染的:

    private func wj_radius() {
         // 加了背景顏色會(huì)觸發(fā)離屏渲染, 其實(shí)設(shè)置layer的backgroundColor
        imageV.backgroundColor = .red
        imageV.layer.cornerRadius = 20
        imageV.clipsToBounds = true
        
        // 它設(shè)置的是contents這個(gè)backgroundColor
        label.backgroundColor = .red
        label.layer.cornerRadius = 15
        label.clipsToBounds = true
    }
image.png

其實(shí)label.backgroundColor = .red其實(shí)設(shè)置的是contents層的背景色循签,這與imageV不一樣了县匠,如何看出呢撒轮?我們設(shè)置label.layer的背景色看看輸出:

    private func wj_radius() {
         // 加了背景顏色會(huì)觸發(fā)離屏渲染, 其實(shí)設(shè)置layer的backgroundColor
        imageV.backgroundColor = .red
        imageV.layer.cornerRadius = 20
        imageV.clipsToBounds = true
        
        // 它設(shè)置的是contents這個(gè)backgroundColor
        label.backgroundColor = .red
        // 它設(shè)置的是layer的backgroundColor
        label.layer.backgroundColor = UIColor.blue.cgColor
        label.layer.cornerRadius = 15
        label.clipsToBounds = true
    }
image.png

可以看出label的背景色依舊是紅色兰粉,并且這個(gè)時(shí)候label是通過離屏渲染過程的臀蛛。(另外label設(shè)置邊框顏色和寬度也會(huì)離屏渲染 自行調(diào)試)

圓角離屏渲染總結(jié):設(shè)置圓角要觸發(fā)離屏渲染條件:去操作contents

圓角優(yōu)化 - 貝塞爾曲線
    private func wj_radiusBezier() {
        imageV.backgroundColor = .red        
        UIGraphicsBeginImageContextWithOptions(imageV.bounds.size, false, 0.0)
        UIBezierPath(roundedRect: imageV.bounds, cornerRadius: imageV.bounds.size.height/2).addClip()
        imageV.draw(imageV.bounds)
        imageV.image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
    }

然而這種優(yōu)化方案不是最優(yōu)異的解決辦法。根據(jù)實(shí)際情況客峭,可以讓設(shè)計(jì)師去切一個(gè)圓形的遮罩圖舔琅,當(dāng)然這種方式并不能在所有場景都適用洲劣。大家靈活運(yùn)用。

圓形的遮罩圖.png

特殊的離屏渲染

視圖在重寫drawRect方法的時(shí)候郊尝,系統(tǒng)是通過CoreGraphics框架去渲染流昏,它是由cpu去計(jì)算工作的。重寫drawRect方法會(huì)另外開啟一個(gè)內(nèi)存空間去生成另一塊畫布谚鄙。
重寫drawRect方法是無法通過檢測器去檢測到的闷营。所以它是特殊的離屏渲染知市。

本文重在對(duì)離屏渲染分析。demo鏈接
喜歡的鐵鐵給個(gè)star支持一下莫杈,感謝收看。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末腥光,一起剝皮案震驚了整個(gè)濱河市糊秆,隨后出現(xiàn)的幾起案子痘番,更是在濱河造成了極大的恐慌,老刑警劉巖伍纫,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件莹规,死亡現(xiàn)場離奇詭異泌神,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)母市,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門患久,熙熙樓的掌柜王于貴愁眉苦臉地迎上來墙杯,“玉大人,你說我怎么就攤上這事溉旋〖邓瑁” “怎么了算行?”我有些...
    開封第一講書人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵州邢,是天一觀的道長。 經(jīng)常有香客問我骗村,道長呀枢,這世上最難降的妖魔是什么裙秋? 我笑而不...
    開封第一講書人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任摘刑,我火速辦了婚禮,結(jié)果婚禮上即彪,老公的妹妹穿的比我還像新娘活尊。我一直安慰自己,他們只是感情好深胳,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開白布舞终。 她就那樣靜靜地躺著敛劝,像睡著了一般余爆。 火紅的嫁衣襯著肌膚如雪蛾方。 梳的紋絲不亂的頭發(fā)上上陕,一...
    開封第一講書人閱讀 49,144評(píng)論 1 285
  • 那天释簿,我揣著相機(jī)與錄音,去河邊找鬼煮纵。 笑死醉途,一個(gè)胖子當(dāng)著我的面吹牛砖茸,可吹牛的內(nèi)容都是我干的凉夯。 我是一名探鬼主播采幌,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼休傍,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了人柿?” 一聲冷哼從身側(cè)響起凫岖,我...
    開封第一講書人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤哥放,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后踩身,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體社露,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡呵哨,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年孟害,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了挨务。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡丁侄,死狀恐怖鸿摇,靈堂內(nèi)的尸體忽然破棺而出劈猿,到底是詐尸還是另有隱情揪荣,我是刑警寧澤,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布佛舱,位于F島的核電站请祖,受9級(jí)特大地震影響凰棉,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜福压,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一荆姆、第九天 我趴在偏房一處隱蔽的房頂上張望胆筒。 院中可真熱鬧,春花似錦抒和、人聲如沸彤蔽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至卖哎,卻和暖如春虏束,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留群发,地道東北人熟妓。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓起愈,卻偏偏與公主長得像译仗,于是被迫代替她去往敵國和親官觅。 傳聞我的和親對(duì)象是個(gè)殘疾皇子休涤,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容