開始前的提問:
1.離屏渲染是什么秆撮?
2.離屏渲染在哪一步進(jìn)行的四濒?
3.離屏渲染的影響在哪?
4.設(shè)置圓角一定會(huì)觸發(fā)離屏渲染嗎职辨?
5.如何優(yōu)化離屏渲染盗蟆?
深入理解了上面幾個(gè)問題足以回答面試官的問題。
iOS中圖像渲染流程
UIKit
其實(shí)就是CoreGraphics
和CoreAnimation
的高度集成舒裤。
我們通過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
做一些濾鏡處理操作
在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ù)顯示出來岩调。
視頻控制器是怎么把幀緩存區(qū)的數(shù)據(jù)顯示出來的?
通過逐行掃描的方式確定一幀畫面的顯示赡盘。再回到初始的點(diǎn)逐行掃描確定下一幀的圖像号枕。
我們的顯示器通常都是固定的形式刷新的。像我們蘋果手機(jī)刷新頻率每秒60Hz陨享。我們做屏幕優(yōu)化的時(shí)候都是以fps作為指標(biāo)葱淳。那么它的值越接近60說明屏幕流暢度越好
當(dāng)VSync垂直同步信號(hào)的時(shí)候,GPU它還沒有把這一幀的數(shù)據(jù)放入frameBuffer抛姑,我們這一個(gè)畫面幀就會(huì)被丟失赞厕。等待下一個(gè)垂直同步信號(hào)過來的時(shí)候,再來顯示我們前面的內(nèi)容定硝。
離屏渲染是什么皿桑?
如果要在顯示屏上顯示內(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è)過程被稱之為離屏渲染
近零。
當(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)化手段
UIView
和CALayer
關(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。
以下離屏渲染操作岸军,按對(duì)性能影響等級(jí)從高到低進(jìn)行排序: shadows(陰影)
奋刽、圓角
、mask(遮罩)
艰赞、allowsGroupOpacity(組不透明)
佣谐、edge antialiasing(抗鋸齒)
離屏渲染分析
如何查看渲染出來的UI是否經(jīng)過離屏渲染過程呢?
打開模擬器工具欄
-> Debug
-> Color Off-screen Rendered
圖層變化成黃色方妖,說明是經(jīng)過離屏渲染狭魂。
代碼部分很簡單,新建一個(gè)工程寫一個(gè)tableView,在tableView上面添加一個(gè)ImageView和一個(gè)UILabel即可雌澄。接下來逐個(gè)分析出現(xiàn)離屏渲染的情況斋泄。
Instruments使用
利用Core Animation
來分析應(yīng)用的性能問題
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
}
遮罩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
}
圓角案例二
// 四個(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
}
圓角案例三
// 添加子視圖 - 四個(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
}
圓角案例四:重點(diǎn)分析
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è)置的是layer
的backgroundColor
泌辫。
此時(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
}
這是因?yàn)?imageV.image = nil
實(shí)際上是把imageV
的contents層
內(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
}
這就驗(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
}
其實(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
}
可以看出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)用。
特殊的離屏渲染
視圖在重寫drawRect
方法的時(shí)候郊尝,系統(tǒng)是通過CoreGraphics
框架去渲染流昏,它是由cpu去計(jì)算工作的。重寫drawRect
方法會(huì)另外開啟一個(gè)內(nèi)存空間去生成另一塊畫布谚鄙。
重寫drawRect
方法是無法通過檢測器去檢測到的闷营。所以它是特殊的離屏渲染知市。
本文重在對(duì)離屏渲染分析。demo鏈接
喜歡的鐵鐵給個(gè)star支持一下莫杈,感謝收看。