iOS 常見觸發(fā)離屏渲染場(chǎng)景及優(yōu)化方案總結(jié)

以下方案,常用的陰影、圓角等經(jīng)過筆者測(cè)試可行,剩余場(chǎng)景方案僅供參考,并未實(shí)際測(cè)試

對(duì)什么是離屏渲染,以及為什么會(huì)產(chǎn)生離屏渲染尚不了解的建議看看四、深入剖析【離屏渲染】原理這篇文章,再來閱讀本文

在離屏渲染觸發(fā)的場(chǎng)景中,按照性能影響從高到低排序,如下所示

  • shadows(陰影)
  • conerRadius > 0 + maskToBounds = true(常見的圓角設(shè)置手段)
  • mask(遮罩)
  • allowsGroupOpacity(組不透明)
  • edge antialiasing(抗鋸齒)

下面針對(duì)不同場(chǎng)景說明為什么以及怎么解決離屏渲染問題

添加了陰影的layer(layer.shadow)

  • layer本身是一塊矩形區(qū)域,而陰影是作用于在整個(gè)非透明區(qū)域,并顯示在所有l(wèi)ayer的最下方。
  • 根據(jù)畫家算法,由遠(yuǎn)及近的渲染,陰影是第一個(gè)被渲染的,但是陰影渲染有一個(gè)前提:我們必須畫完所有的layer和子layer后。
  • 所以這時(shí)我們就需要一個(gè)臨時(shí)緩存,這個(gè)緩存區(qū)就是離屏緩沖區(qū),用來將所有l(wèi)ayer都渲染完成,再根據(jù)所有l(wèi)ayer和子layer組合后的圖層的形狀,添加陰影到FrameBuffer,最后顯示到屏幕上

下面我們以按鈕的陰影來進(jìn)行演示

        let btn0 = UIButton(type: .custom)
        btn0.frame = CGRect(x: 100, y: 60, width: 100, height: 100)
        //設(shè)置圓角
        self.view.addSubview(btn0)
        //設(shè)置背景圖片
        btn0.setImage(UIImage(named: "mouse"), for: .normal)
        //陰影
        btn0.layer.shadowColor = UIColor.lightGray.cgColor
        btn0.layer.shadowOpacity = 1.0
        btn0.layer.shadowRadius = 2.0
        btn0.layer.shadowOffset = CGSize(width: 5, height: 5)

根據(jù)上面這段代碼的運(yùn)行,可以看到如下結(jié)果,設(shè)置陰影時(shí)是觸發(fā)了離屏渲染的


shadow離屏渲染效果

優(yōu)化方案
使用陰影必須保證 layer 的masksToBounds = false,因此陰影與系統(tǒng)圓角不兼容。但是注意,只是在視覺上看不到,對(duì)性能的影響依然。通常使用指定路徑來避免離屏渲染

  • 方案1:指定路徑
    在上述代碼的基礎(chǔ)上增加以下兩行代碼
        //指定路徑 - 避免離屏渲染
        let path = UIBezierPath(rect: btn0.bounds)
        btn0.layer.shadowPath = path.cgPath

效果如下


shadow避免離屏渲染的結(jié)果

除了指定路徑,還有其他解決方案,不過筆者并沒有一一試驗(yàn),有興趣的可以自己嘗試下

  • 利用混合圖層模擬陰影的效果
sublayer.contents = (id)[UIImage imageNamed:@"xxx"].CGImage;
[view.layer addSublayer:sublayer];
  • 利用一個(gè)陰影效果的視圖添加在需要顯示陰影的位置
  • 使用 Core Graphics 繪制陰影

需要進(jìn)行裁剪的layer(layer.masksToBounds / view.clipsToBounds)

這種場(chǎng)景就是我們常用的圓角處理,當(dāng)我們需要繪制一個(gè)帶有圓角并且需要剪切圓角以外內(nèi)容的容器時(shí),就會(huì)觸發(fā)離屏渲染,例如UIButton、UIImageView等

注意:iOS官方針對(duì)UIImageView有一些優(yōu)化,
==> 在iOS9之前,UIImageView和UIButton通過cornerRadius+masksToBounds設(shè)置圓角都會(huì)觸發(fā)離屏渲染,
==> 但是UIImageView在ios9以后,針對(duì)UIImageView中的image設(shè)置圓角并不會(huì)觸發(fā)離屏渲染,如果加上了背景色或者陰影等其他效果還是會(huì)觸發(fā)離屏渲染的

優(yōu)化方案
對(duì)于content無內(nèi)容或者內(nèi)容非背景透明(不涉及到圓角以外的區(qū)域)的layer,直接設(shè)置layer的backgroundColor + cornerRadius 屬性繪制圓角

常用的優(yōu)化方案參見iOS 常用的圓角處理方式總結(jié)

下面再補(bǔ)充一些其他方案

  • 后臺(tái)繪制,前臺(tái)設(shè)置圖片
- (void)setCircleImage
{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        UIImage * circleImage = [image imageWithCircle];
        dispatch_async(dispatch_get_main_queue(), ^{
            imageView.image = circleImage;
        });
    });
}


#import "UIImage+Addtions.h"
@implementation UIImage (Addtions)
//返回一張圓形圖片
- (instancetype)imageWithCircle
{
    UIGraphicsBeginImageContextWithOptions(self.size, NO, 0);
    UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, self.size.width, self.size.height)];
    [path addClip];
    [self drawAtPoint:CGPointZero];
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return image;
}
  • 使用混合圖層,在layer上方疊加相應(yīng)mask形狀的半透明layer
sublayer.contents = (id)[UIImage imageNamed:@"xxx"].CGImage;
[view.layer addSublayer:sublayer];

使用了mask的layer(layer.mask)

  • mask是覆蓋在所有l(wèi)ayer及其子layer之上的,可能還帶有一定的透明度。
  • mask也是需要等整個(gè)layer樹繪制完成,再加上mask和組合后的lzyer進(jìn)行組合,所以需要開辟一個(gè)獨(dú)立于FrameBuffer的內(nèi)存,用于將layer及其子layer畫完,最后再和mask進(jìn)行組合,存儲(chǔ)到FrameBuffer,視頻控制器從FrameBuffer中讀取數(shù)據(jù)顯示到屏幕上

優(yōu)化方案

  • 不使用mask,使用混合圖層,在layer上方疊加相應(yīng)mask形狀的半透明layer

設(shè)置了組透明度為 YES,并且透明度不為 1 的 layer (layer.allowsGroupOpacity/ layer.opacity)

  • group opacity中alpha并不是分別應(yīng)用到每一層之上,需要整個(gè)layer樹畫完之后,在統(tǒng)一加上alpha,和底層其他layer的像素進(jìn)行組合,此時(shí)顯然無法通過一次遍歷就得到結(jié)果
  • 需要另外開啟一個(gè)獨(dú)立內(nèi)存,先將layer及其子layer畫好,最后給組合后的圖層加上alpha進(jìn)行渲染,將最終結(jié)果存儲(chǔ)到幀緩沖區(qū)
  • GroupOpacity 開啟離屏渲染的條件是:layer.opacity != 1.0并且有子 layer 或者背景圖。

優(yōu)化方案
關(guān)閉allowsGroupOpacity屬性,根據(jù)產(chǎn)品需求自己控制layer透明度

采用了光柵化的 layer (layer.shouldRasterize)

如果layer的layer.shouldRasterize被設(shè)置為true,會(huì)在觸發(fā)離屏渲染的同時(shí),將光柵化后的內(nèi)容緩存起來,如果在下一次,對(duì)應(yīng)的layer和子layer沒有改變,則復(fù)用離屏緩沖區(qū)的結(jié)果,可以很大程度提升性能

  • 當(dāng)視圖內(nèi)容是靜態(tài)不變時(shí),設(shè)置 shouldRasterize(光柵化)為YES,此方案最為實(shí)用方便。
view.layer.shouldRasterize = true;
view.layer.rasterizationScale = view.layer.contentsScale;

  • 但當(dāng)視圖內(nèi)容是動(dòng)態(tài)變化(如后臺(tái)下載圖片完畢后切換到主線程設(shè)置)時(shí),使用此方案反而為增加系統(tǒng)負(fù)荷。

繪制了文字的 layer (UILabel, CATextLayer, Core Text 等)

想要在 UILabel 和 UITextView 上實(shí)現(xiàn)低成本的圓角(不觸發(fā)離屏渲染),需要保證 layer 的contents呈現(xiàn)透明的背景色,文本視圖類的 layer 的contents默認(rèn)是透明的(字符就在這個(gè)透明的環(huán)境里繪制、顯示),此時(shí)只需要設(shè)置 layer 的backgroundColor,再加上cornerRadius就可以搞定了。不過 UILabel 上設(shè)置backgroundColor的行為被更改了,不再是設(shè)定 layer 的背景色而是為contents設(shè)置背景色,UITextView 則沒有改變這一點(diǎn)
不要直接利用label.backgroundColor = aColor 設(shè)置背景色
-不要直接在XIB中為label設(shè)置背景色
(感謝小K仔仔仔指出這里的問題)

在這里重新做下梳理,經(jīng)過測(cè)試

  • 設(shè)置label.backgroundColor + cornerradius ,圓角的裁剪并沒有l(wèi)abel設(shè)置的backgroundColor上,這種背景顏色的設(shè)置方式其實(shí)是為contents設(shè)置背景色,而contents圓角的裁剪需要設(shè)置 masksToBounds 才會(huì)生效。但是這種方式也并沒有觸發(fā)離屏渲染,
        let label = UILabel(frame: CGRect(x: 100, y: 100, width: 100, height: 50))
        label.text = "測(cè)試"
        label.backgroundColor = UIColor.lightGray
        label.layer.cornerRadius = 8
        label.layer.masksToBounds = true
        self.view.addSubview(label)
效果

==> 猜測(cè)原因可能是這樣的,我們?cè)O(shè)置的背景色只是為contents設(shè)置的,所以圓角的裁剪其實(shí)針對(duì)的也只是contents,相當(dāng)于此時(shí)只有一個(gè)圖層需要設(shè)置圓角,所以并不會(huì)觸發(fā)離屏渲染

  • label中設(shè)置label3.layer.backgroundColor + cornerradius,此時(shí)也不會(huì)觸發(fā)離屏渲染,原因同一種情況類似

UILabel中不會(huì)觸發(fā)離屏渲染的圓角化方案

  • UILabel直接通過label的layer設(shè)置背景色 + cornerRadius
label.layer.backgroundColor = aColor
label.layer.cornerRadius = 5
  • label3.backgroundColor + cornerradius + masksToBounds

UILabel哪些情況會(huì)觸發(fā)離屏渲染?
大致測(cè)試了下,有以下兩種情況

  • UILabel中添加了子視圖,且需要圓角化,類似下面這種
let label3 = UILabel(frame: CGRect(x: 100, y: 350
            , width: 100, height: 50))
       label3.text = "測(cè)試3"
       let view3 = UIView(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
       view3.backgroundColor = UIColor.red
       label3.addSubview(view3)
        label3.layer.backgroundColor = UIColor.lightGray.cgColor
       label3.layer.cornerRadius = 8
       label3.layer.masksToBounds = true
       self.view.addSubview(label3)

此時(shí)不論是否設(shè)置label3.layer.backgroundColor,都會(huì)觸發(fā)離屏渲染,如圖所示

設(shè)置了layer的背景色

未設(shè)置layer的背景色

  • 設(shè)置label.layer.backgroundColor + cornerradius時(shí),同時(shí)設(shè)置了 masksToBounds,也會(huì)觸發(fā)離屏渲染,所以在寫代碼時(shí),要注意,label不復(fù)雜時(shí),僅設(shè)置
    label.layer.backgroundColor + cornerradius即可圓角化。

使用高斯模糊(毛玻璃)效果

ios屏幕顯示推送通知頁面或者UIVisualEffectView

edge antialiasing(抗鋸齒)

不設(shè)置 allowsEdgeAntialiasing 屬性為YES(默認(rèn)為NO)

總結(jié)

  • RoundedCorner(圓角) 在僅指定cornerRadius時(shí)不會(huì)觸發(fā)離屏渲染,僅適用于特殊情況:contents為 nil 或者contents不會(huì)遮擋背景色時(shí)的圓角。
  • Shawdow 可以通過指定路徑來取消離屏渲染。
  • Mask 無法取消離屏渲染,可以利用混合圖層來進(jìn)行優(yōu)化。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容