iOS 常见触发离屏渲染场景及优化方案总结

以下方案,常用的阴影、圆角等经过笔者测试可行,剩余场景方案仅供参考,并未实际测试

对什么是离屏渲染,以及为什么会产生离屏渲染尚不了解的建议看看四、深入剖析【离屏渲染】原理这篇文章,再来阅读本文

在离屏渲染触发的场景中,按照性能影响从高到低排序,如下所示

  • shadows(阴影)
  • conerRadius > 0 + maskToBounds = true(常见的圆角设置手段)
  • mask(遮罩)
  • allowsGroupOpacity(组不透明)
  • edge antialiasing(抗锯齿)

下面针对不同场景说明为什么以及怎么解决离屏渲染问题

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

  • layer本身是一块矩形区域,而阴影是作用于在整个非透明区域,并显示在所有layer的最下方。
  • 根据画家算法,由远及近的渲染,阴影是第一个被渲染的,但是阴影渲染有一个前提:我们必须画完所有的layer和子layer后。
  • 所以这时我们就需要一个临时缓存,这个缓存区就是离屏缓冲区,用来将所有layer都渲染完成,再根据所有layer和子layer组合后的图层的形状,添加阴影到FrameBuffer,最后显示到屏幕上

下面我们以按钮的阴影来进行演示

        let btn0 = UIButton(type: .custom)
        btn0.frame = CGRect(x: 100, y: 60, width: 100, height: 100)
        //设置圆角
        self.view.addSubview(btn0)
        //设置背景图片
        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)

根据上面这段代码的运行,可以看到如下结果,设置阴影时是触发了离屏渲染的


shadow离屏渲染效果

优化方案
使用阴影必须保证 layer 的masksToBounds = false,因此阴影与系统圆角不兼容。但是注意,只是在视觉上看不到,对性能的影响依然。通常使用指定路径来避免离屏渲染

  • 方案1:指定路径
    在上述代码的基础上增加以下两行代码
        //指定路径 - 避免离屏渲染
        let path = UIBezierPath(rect: btn0.bounds)
        btn0.layer.shadowPath = path.cgPath

效果如下


shadow避免离屏渲染的结果

除了指定路径,还有其他解决方案,不过笔者并没有一一试验,有兴趣的可以自己尝试下

  • 利用混合图层模拟阴影的效果
sublayer.contents = (id)[UIImage imageNamed:@"xxx"].CGImage;
[view.layer addSublayer:sublayer];
  • 利用一个阴影效果的视图添加在需要显示阴影的位置
  • 使用 Core Graphics 绘制阴影

需要进行裁剪的layer(layer.masksToBounds / view.clipsToBounds)

这种场景就是我们常用的圆角处理,当我们需要绘制一个带有圆角并且需要剪切圆角以外内容的容器时,就会触发离屏渲染,例如UIButton、UIImageView等

注意:iOS官方针对UIImageView有一些优化,
==> 在iOS9之前,UIImageView和UIButton通过cornerRadius+masksToBounds设置圆角都会触发离屏渲染,
==> 但是UIImageView在ios9以后,针对UIImageView中的image设置圆角并不会触发离屏渲染,如果加上了背景色或者阴影等其他效果还是会触发离屏渲染的

优化方案
对于content无内容或者内容非背景透明(不涉及到圆角以外的区域)的layer,直接设置layer的backgroundColor + cornerRadius 属性绘制圆角

常用的优化方案参见iOS 常用的圆角处理方式总结

下面再补充一些其他方案

  • 后台绘制,前台设置图片
- (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上方叠加相应mask形状的半透明layer
sublayer.contents = (id)[UIImage imageNamed:@"xxx"].CGImage;
[view.layer addSublayer:sublayer];

使用了mask的layer(layer.mask)

  • mask是覆盖在所有layer及其子layer之上的,可能还带有一定的透明度。
  • mask也是需要等整个layer树绘制完成,再加上mask和组合后的lzyer进行组合,所以需要开辟一个独立于FrameBuffer的内存,用于将layer及其子layer画完,最后再和mask进行组合,存储到FrameBuffer,视频控制器从FrameBuffer中读取数据显示到屏幕上

优化方案

  • 不使用mask,使用混合图层,在layer上方叠加相应mask形状的半透明layer

设置了组透明度为 YES,并且透明度不为 1 的 layer (layer.allowsGroupOpacity/ layer.opacity)

  • group opacity中alpha并不是分别应用到每一层之上,需要整个layer树画完之后,在统一加上alpha,和底层其他layer的像素进行组合,此时显然无法通过一次遍历就得到结果
  • 需要另外开启一个独立内存,先将layer及其子layer画好,最后给组合后的图层加上alpha进行渲染,将最终结果存储到帧缓冲区
  • GroupOpacity 开启离屏渲染的条件是:layer.opacity != 1.0并且有子 layer 或者背景图。

优化方案
关闭allowsGroupOpacity属性,根据产品需求自己控制layer透明度

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

如果layer的layer.shouldRasterize被设置为true,会在触发离屏渲染的同时,将光栅化后的内容缓存起来,如果在下一次,对应的layer和子layer没有改变,则复用离屏缓冲区的结果,可以很大程度提升性能

  • 当视图内容是静态不变时,设置 shouldRasterize(光栅化)为YES,此方案最为实用方便。
view.layer.shouldRasterize = true;
view.layer.rasterizationScale = view.layer.contentsScale;

  • 但当视图内容是动态变化(如后台下载图片完毕后切换到主线程设置)时,使用此方案反而为增加系统负荷。

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

想要在 UILabel 和 UITextView 上实现低成本的圆角(不触发离屏渲染),需要保证 layer 的contents呈现透明的背景色,文本视图类的 layer 的contents默认是透明的(字符就在这个透明的环境里绘制、显示),此时只需要设置 layer 的backgroundColor,再加上cornerRadius就可以搞定了。不过 UILabel 上设置backgroundColor的行为被更改了,不再是设定 layer 的背景色而是为contents设置背景色,UITextView 则没有改变这一点
不要直接利用label.backgroundColor = aColor 设置背景色
-不要直接在XIB中为label设置背景色
(感谢小K仔仔仔指出这里的问题)

在这里重新做下梳理,经过测试

  • 设置label.backgroundColor + cornerradius ,圆角的裁剪并没有label设置的backgroundColor上,这种背景颜色的设置方式其实是为contents设置背景色,而contents圆角的裁剪需要设置 masksToBounds 才会生效。但是这种方式也并没有触发离屏渲染,
        let label = UILabel(frame: CGRect(x: 100, y: 100, width: 100, height: 50))
        label.text = "测试"
        label.backgroundColor = UIColor.lightGray
        label.layer.cornerRadius = 8
        label.layer.masksToBounds = true
        self.view.addSubview(label)
效果

==> 猜测原因可能是这样的,我们设置的背景色只是为contents设置的,所以圆角的裁剪其实针对的也只是contents,相当于此时只有一个图层需要设置圆角,所以并不会触发离屏渲染

  • label中设置label3.layer.backgroundColor + cornerradius,此时也不会触发离屏渲染,原因同一种情况类似

UILabel中不会触发离屏渲染的圆角化方案

  • UILabel直接通过label的layer设置背景色 + cornerRadius
label.layer.backgroundColor = aColor
label.layer.cornerRadius = 5
  • label3.backgroundColor + cornerradius + masksToBounds

UILabel哪些情况会触发离屏渲染?
大致测试了下,有以下两种情况

  • UILabel中添加了子视图,且需要圆角化,类似下面这种
let label3 = UILabel(frame: CGRect(x: 100, y: 350
            , width: 100, height: 50))
       label3.text = "测试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)

此时不论是否设置label3.layer.backgroundColor,都会触发离屏渲染,如图所示

设置了layer的背景色

未设置layer的背景色

  • 设置label.layer.backgroundColor + cornerradius时,同时设置了 masksToBounds,也会触发离屏渲染,所以在写代码时,要注意,label不复杂时,仅设置
    label.layer.backgroundColor + cornerradius即可圆角化。

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

ios屏幕显示推送通知页面或者UIVisualEffectView

edge antialiasing(抗锯齿)

不设置 allowsEdgeAntialiasing 属性为YES(默认为NO)

总结

  • RoundedCorner(圆角) 在仅指定cornerRadius时不会触发离屏渲染,仅适用于特殊情况:contents为 nil 或者contents不会遮挡背景色时的圆角。
  • Shawdow 可以通过指定路径来取消离屏渲染。
  • Mask 无法取消离屏渲染,可以利用混合图层来进行优化。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,377评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,390评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,967评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,344评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,441评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,492评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,497评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,274评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,732评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,008评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,184评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,837评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,520评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,156评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,407评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,056评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,074评论 2 352