我們看到的屏幕上的數(shù)據(jù)展示有兩種加載流程:
1末购、正常渲染加載流程
2破喻、離屏渲染加載流程
如下圖所示:
可以看出,離屏渲染比正常渲染多了一個(gè)離屏緩沖區(qū)盟榴,這個(gè)緩沖區(qū)的作用是什么呢曹质?為什么要加這個(gè)緩沖區(qū)呢?
首先擎场,說(shuō)說(shuō)正常渲染流程
正常渲染流程
APP中的數(shù)據(jù)經(jīng)過(guò)CPU計(jì)算和GPU渲染后羽德,將結(jié)果存放在幀緩沖區(qū),利用視頻控制器從幀緩沖區(qū)中取出迅办,并顯示到屏幕上宅静。
在GPU的渲染流程中,顯示到屏幕上的圖像是遵循畫(huà)家算法按照由遠(yuǎn)及近的順序站欺,依次將結(jié)果存儲(chǔ)到幀緩沖區(qū)
視頻控制器從幀緩沖區(qū)中讀取一幀數(shù)據(jù)姨夹,將其顯示到屏幕上后,會(huì)立即丟棄這幀數(shù)據(jù)镊绪,不會(huì)做任何保留匀伏,這樣做的目的是可以節(jié)省空間,且在屏幕上是各自顯示各自的蝴韭,互相不影響。
離屏渲染流程
當(dāng)App需要進(jìn)行額外的渲染和合并時(shí)熙侍,例如按鈕設(shè)置圓角榄鉴,我們是需要對(duì)UIButton這個(gè)控件中的所有圖層都進(jìn)行圓角+裁剪,然后再將合并后的結(jié)果存入幀緩存區(qū)蛉抓,再?gòu)膸彺嬷腥〕鼋挥善聊伙@示庆尘,這時(shí),在正常的渲染流程中巷送,我們是無(wú)法做到對(duì)所有圖層進(jìn)行圓角裁剪的驶忌,因?yàn)樗怯靡粋€(gè)丟一個(gè)。所以我們需要提前將處理好的結(jié)果放入離屏緩沖區(qū)笑跛,最后將幾個(gè)圖層進(jìn)行疊加合并付魔,存放到幀緩沖區(qū),最后屏幕上就是我們想實(shí)現(xiàn)的效果飞蹂。
由此可以看出几苍,離屏緩存區(qū)就是一個(gè)臨時(shí)的緩沖區(qū),用來(lái)存放在后續(xù)操作使用陈哑,但目前并不使用的數(shù)據(jù)妻坝。
離屏渲染再給我們帶來(lái)方便的同時(shí)伸眶,也帶來(lái)了嚴(yán)重的性能問(wèn)題。由于離屏渲染中的離屏緩沖區(qū)刽宪,是額外開(kāi)辟的一個(gè)存儲(chǔ)空間厘贼,當(dāng)它將數(shù)據(jù)轉(zhuǎn)存到Frame Buffer時(shí),也是需要耗費(fèi)時(shí)間的圣拄,所以在轉(zhuǎn)存的過(guò)程中涂臣,仍有掉幀的可能。
離屏緩沖區(qū)的空間并不是無(wú)限大的售担,最大只能是屏幕的2.5倍
那為什么我們明知有性能問(wèn)題時(shí)赁遗,還是要使用離屏渲染呢?
可以處理一些特殊的效果族铆,這種效果并不能一次就完成岩四,需要使用離屏緩沖區(qū)來(lái)保存中間狀態(tài),不得不使用離屏渲染哥攘,這種情況下的離屏渲染是系統(tǒng)自動(dòng)觸發(fā)的剖煌,例如經(jīng)常使用的圓角、陰影逝淹、高斯模糊耕姊、光柵化等
可以提升渲染的效率,如果一個(gè)效果是多次實(shí)現(xiàn)的栅葡,可以提前渲染装悲,保存到離屏緩沖區(qū),以達(dá)到復(fù)用的目的签杈。這種情況是需要開(kāi)發(fā)者手動(dòng)觸發(fā)的翘狱。
注意:通常會(huì)觸發(fā)離屏渲染,除了圓角熊咽、剪裁外莫鸭,還有設(shè)置了透明度+組透明(layer.allowsGroupOpacity+layer.opacity),陰影屬性(shadowOffset 等)因?yàn)榻M透明度横殴、陰影都是和裁剪類似的被因,會(huì)作用與 layer 以及其所有 sublayer 上,這就導(dǎo)致必然會(huì)引起離屏渲染衫仑。
離屏渲染的另一個(gè)情況:光柵化(shouldRasterize )
官方文檔
由文檔中我們可以看出梨与,當(dāng)開(kāi)啟光柵化時(shí),會(huì)將layer渲染成位圖保存在緩存中惑畴,這樣在下次使用時(shí)蛋欣,就可以直接復(fù)用,提高效率如贷。
shouldRasterize使用建議:
layer不復(fù)用陷虎,沒(méi)必要使用shouldRasterize
layer不是靜態(tài)的到踏,也就是說(shuō)要頻繁的進(jìn)行修改,沒(méi)必要使用shouldRasterize
時(shí)間方面:離屏渲染緩存有100ms時(shí)間限制尚猿,超過(guò)該時(shí)間的內(nèi)容會(huì)被丟棄窝稿,進(jìn)而不能達(dá)到復(fù)用的目的
空間方面:離屏渲染空間是屏幕像素的2.5倍,如果超過(guò)也無(wú)法復(fù)用凿掂。
圓角與離屏渲染
首先說(shuō)明下CALayer的構(gòu)成伴榔,如圖所示,它是由backgroundColor庄萎、contents踪少、borderWidth&borderColor構(gòu)成的。跟我們即將研究的圓角觸發(fā)離屏渲染息息相關(guān)糠涛。
圓角設(shè)置不生效問(wèn)題援奢!
在平常寫(xiě)代碼時(shí),比如UIButton設(shè)置圓角忍捡,當(dāng)設(shè)置好按鈕的image集漾、cornerRadius、borderWidth砸脊、borderColor等屬性后具篇,運(yùn)行發(fā)現(xiàn)并沒(méi)有實(shí)現(xiàn)我們想要的效果
let btn = UIButton(type: .custom)
btn.frame = CGRect(x: 100, y: 60, width: 100, height: 100)
//設(shè)置圓角
btn.layer.cornerRadius = 50
//設(shè)置border寬度和顏色
btn.layer.borderWidth = 2
btn.layer.borderColor = UIColor.red.cgColor
self.view.addSubview(btn0)
//設(shè)置背景圖片
btn.setImage(UIImage(named: "cat"), for: .normal)
當(dāng)我們運(yùn)行時(shí),我們發(fā)現(xiàn)沒(méi)有效果如下圖:
看到上面的情況大家都會(huì)設(shè)置masksToBounds為 true凌埂,解決的方法很簡(jiǎn)單驱显,但原理是大部人都沒(méi)有去仔細(xì)研究的。
讓我們看看蘋(píng)果官方文檔:
官方文檔告訴我們侨舆,設(shè)置cornerRadius只會(huì)對(duì)CALayer中的backgroundColor 和 boder設(shè)置圓角秒紧,不會(huì)設(shè)置contents的圓角,如果contents需要設(shè)置圓角挨下,需要同時(shí)將maskToBounds / clipsToBounds設(shè)置為true。
所以我們可以理解為圓角不生效的根本原因是沒(méi)有對(duì)contents設(shè)置圓角脐湾,而按鈕設(shè)置的image是放在contents里面的臭笆,所以看到的界面上的就是image沒(méi)有進(jìn)行圓角裁剪。
下面我們通過(guò)幾段代碼來(lái)說(shuō)明 圓角設(shè)置中什么時(shí)候會(huì)離屏渲染觸發(fā)
首先秤掌,需要打開(kāi)模擬器的離屏渲染顏色標(biāo)記
1愁铺、按鈕 僅設(shè)置背景顏色+border
let btn01 = UIButton(type: .custom)
btn01.frame = CGRect(x: 100, y: 200, width: 100, height: 100)
//設(shè)置圓角
btn01.layer.cornerRadius = 50
//設(shè)置border寬度和顏色
btn01.layer.borderWidth = 4
btn01.layer.borderColor = UIColor.blue.cgColor
self.view.addSubview(btn01)
//設(shè)置背景顏色
btn01.backgroundColor = UIColor.yellow
在這種情況下,無(wú)論是使用默認(rèn)的maskToBounds / clipsToBounds(false)闻鉴,還是將其修改為true茵乱,都不會(huì)觸發(fā)離屏渲染,究其根本原因是 contents中沒(méi)有需要圓角處理的layer孟岛。
2瓶竭、按鈕設(shè)置背景圖片+boder
let btn0 = UIButton(type: .custom)
btn0.frame = CGRect(x: 100, y: 60, width: 100, height: 100)
//設(shè)置圓角
btn0.layer.cornerRadius = 50
//設(shè)置border寬度和顏色
btn0.layer.borderWidth = 2
btn0.layer.borderColor = UIColor.red.cgColor
self.view.addSubview(btn0)
//設(shè)置背景圖片
btn0.setImage(UIImage(named: "cat.jpg"), for: .normal)
使用默認(rèn)的maskToBounds / clipsToBounds(false)
這種情況就是最開(kāi)始我們講到的圓角設(shè)置不生效的情況督勺,就不再多做說(shuō)明了
maskToBounds / clipsToBounds 修改為true
3、ImageView設(shè)置背景+圓角
let imageView = UIImageView()
imageView.frame = CGRect(x: 100, y: 60, width: 100, height: 100)
imageView.layer.backgroundColor = UIColor.red.cgColor
imageView.layer.cornerRadius = 50
self.view.addSubview(imageView)
這種情況會(huì)得到圓角的且不會(huì)觸發(fā)離屏渲染斤贰。
4智哀、ImageView設(shè)置圖片+圓角
let imageView = UIImageView()
imageView.frame = CGRect(x: 100, y: 60, width: 100, height: 100)
// imageView.layer.backgroundColor = UIColor.red.cgColor
imageView.layer.cornerRadius = 50
imageView.image = UIImage(named: "cat.jpg")
imageView.clipsToBounds = true
self.view.addSubview(imageView)
此時(shí)我們不設(shè)置背景色,這種情況會(huì)得到圓角的且不會(huì)觸發(fā)離屏渲染荧恍。
5瓷叫、ImageView設(shè)置圖片+圓角+背景色
let imageView = UIImageView()
imageView.frame = CGRect(x: 100, y: 60, width: 100, height: 100)
imageView.layer.backgroundColor = UIColor.red.cgColor
imageView.layer.cornerRadius = 50
imageView.image = UIImage(named: "cat.jpg")
imageView.clipsToBounds = true
self.view.addSubview(imageView)
我們發(fā)現(xiàn)這時(shí)得到了圓角但是觸發(fā)了離屏渲染
總結(jié)
當(dāng)只設(shè)置backgroundColor、border送巡,而contents中沒(méi)有子視圖時(shí)摹菠,無(wú)論maskToBounds / clipsToBounds是true還是false,都不會(huì)觸發(fā)離屏渲染
當(dāng)contents中有子視圖時(shí)骗爆,此時(shí)設(shè)置 cornerRadius+maskToBounds / clipsToBounds,就會(huì)觸發(fā)離屏渲染
優(yōu)化次氨、
常見(jiàn)的圓角導(dǎo)致的離屏渲染的處理方法
方案1
_imageView.clipsToBounds=YES;
_imageView.layer.cornerRadius=4.0;
方案2
方案3
方案4
最后,大家看下YYImage的對(duì)于圓角的處理代碼淮腾。
- (UIImage *)yy_imageByRoundCornerRadius:(CGFloat)radius
corners:(UIRectCorner)corners
borderWidth:(CGFloat)borderWidth
borderColor:(UIColor *)borderColor
borderLineJoin:(CGLineJoin)borderLineJoin {
if (corners != UIRectCornerAllCorners) {
UIRectCorner tmp = 0;
if (corners & UIRectCornerTopLeft) tmp |= UIRectCornerBottomLeft;
if (corners & UIRectCornerTopRight) tmp |= UIRectCornerBottomRight;
if (corners & UIRectCornerBottomLeft) tmp |= UIRectCornerTopLeft;
if (corners & UIRectCornerBottomRight) tmp |= UIRectCornerTopRight;
corners = tmp;
}
UIGraphicsBeginImageContextWithOptions(self.size, NO, self.scale);
CGContextRef context = UIGraphicsGetCurrentContext();
CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height);
CGContextScaleCTM(context, 1, -1);
CGContextTranslateCTM(context, 0, -rect.size.height);
CGFloat minSize = MIN(self.size.width, self.size.height);
if (borderWidth < minSize / 2) {
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(rect, borderWidth, borderWidth) byRoundingCorners:corners
cornerRadii:CGSizeMake(radius, borderWidth)];
[path closePath];
CGContextSaveGState(context);
[path addClip];
CGContextDrawImage(context, rect, self.CGImage);
CGContextRestoreGState(context);
}
if (borderColor && borderWidth < minSize / 2 && borderWidth > 0) {
CGFloat strokeInset = (floor(borderWidth * self.scale) + 0.5) / self.scale;
CGRect strokeRect = CGRectInset(rect, strokeInset, strokeInset);
CGFloat strokeRadius = radius > self.scale / 2 ? radius - self.scale / 2 : 0;
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:strokeRect byRoundingCorners:corners cornerRadii:CGSizeMake(strokeRadius,
borderWidth)];
[path closePath];
path.lineWidth = borderWidth;
path.lineJoinStyle = borderLineJoin;
[borderColor setStroke];
}