关于离屏渲染的深入理解

前言

        离屏渲染,这应该是一个老生常谈的话题了。许多人对于离屏渲染都可以说出一二,而且在面试中,离屏渲染也是一个面试官很爱问的问题,比如说为何会出现离屏渲染,如何防止离屏渲染,可能大家都会说减少圆角,减少边框,减少阴影等等。但是,这是这只是冰山一角。
        那么,我们今天就一起来探讨一下离屏渲染

原理

首先我们看一个例子。

    //1.按钮存在背景图片
    UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
    btn1.frame = CGRectMake(100, 30, 100, 100);
    btn1.layer.cornerRadius = 50;
    [self.view addSubview:btn1];
    btn1.layer.shouldRasterize = YES;
    [btn1 setImage:[UIImage imageNamed:@"btn.png"] forState:UIControlStateNormal];
    btn1.layer.masksToBounds = YES;

    //2.按钮不存在背景图片
    UIButton *btn2 = [UIButton buttonWithType:UIButtonTypeCustom];
    btn2.frame = CGRectMake(100, 180, 100, 100);
    btn2.layer.cornerRadius = 50;
    btn2.backgroundColor = [UIColor blueColor];
    [self.view addSubview:btn2];
    btn2.clipsToBounds = YES;

    //3.UIImageView 设置了图片+背景色;
    UIImageView *img1 = [[UIImageView alloc]init];
    img1.frame = CGRectMake(100, 320, 100, 100);
    img1.backgroundColor = [UIColor blueColor];
    [self.view addSubview:img1];
    img1.layer.cornerRadius = 50;
    img1.layer.masksToBounds = YES;
    img1.image = [UIImage imageNamed:@"btn.png"];

    //4.UIImageView 只设置了图片,无背景色;
    UIImageView *img2 = [[UIImageView alloc]init];
    img2.frame = CGRectMake(100, 480, 100, 100);
    [self.view addSubview:img2];
    img2.layer.cornerRadius = 50;
    img2.layer.masksToBounds = YES;
    img2.image = [UIImage imageNamed:@"btn.png"];

这是我们代码中常常会出现的情况。那么,这几种会造成离屏渲染吗?接下来我们通过调试。运行代码。

Simulator -> Debug -> Color Off-screen Rendered

image.png

接下来可以看到结果。


image.png

通过结果可以看到,第一个和第三个图像出现了离屏渲染,别急,待会儿和大家一一解答。

渲染流程

渲染流程对比图.png

通过图片可以看到,和正常的渲染流程不同的是,离屏渲染会把数据放入一个离屏缓冲区(Offscreen Buffer)中,待所有图层的结果进行混合计算完成后才会在屏幕进行展示。


mask.jpg

举一个例子,如上图所示,在一个相机的图标上添加一个遮罩(Mask),总共会经过这么几步:

  • 在App提交到Core Animation
  • 再到渲染服务(Render Server),接下来的工作用OpenGL/Metal来进行操作
  • 相机按钮来说,首先利用顶点着色器(Vertex Shader)绘制顶点,然后进行图元装配(Primitive Assembly),最后到片元着色器(Pixel Shader)进行渲染,最后获得的数据存入离屏缓冲区(Offscreen Buffer)中。 Pass 1
  • 遮罩相机的渲染流程类似,渲染完成后存入另一个离屏缓冲区(Offscreen Buffer)中。 Pass 2
  • Pass 1Pass 2中存入到离屏缓冲区(Offscreen Buffer)的数据拿出来进行渲染从而展示在界面中。

总结,通过以上的流程我们可以看到,Pass 1和Pass 2两次步骤的数据因为要进行合并和渲染,所以在执行完成后并不能进行丢弃,而必须存入离屏缓冲区这个中间变量,所以就要开辟一个空间,而造成离屏渲染
用一句通俗的话讲,离屏渲染出现的原因就是App进行额外的渲染和合并,会将中间过程产生的数据存入离屏缓冲区(Offscreen Buffer),从而造成离屏渲染。

离屏渲染的危害

  • 离屏渲染会开辟一个离屏缓冲区(Offscreen Buffer),所以会占用额外的存入空间
  • 离屏缓冲区(Offscreen Buffer)空间的限制是屏幕像素的2.5倍
  • 从离屏缓冲区(Offscreen Buffer)到帧缓冲区(FrameBuffer)这个过程会造成 时间和性能的损耗。
  • 容易掉帧,造成性能问题。

离屏渲染触发方式

主动触发 -- 开启光栅化(ShouldRasterize)

When the value of this property is true, the layer is rendered as a bitmap in its local coordinate space and then composited to the destination with any other content.

        用一句话概括就是当shouldRasterize设成true时,layer被渲染成一个bitmap,并缓存起来,等下次使用时不会再重新去渲染了。
        
由此可以得知,光栅化开启时,会造成离屏渲染,但是光栅化会对layer进行复用,所以这就是一个很矛盾的点了,离屏渲染会造成性能损耗,但是光栅化又会节约内存,所以到底怎么用光栅化呢?在这里给大家几个建议:

  • 如果layer不能被复用,没有必要打开光栅化
  • 如果layer不是静态的,会被频繁渲染,开启离屏渲染反而影响效率
  • 离屏渲染内容有时间限制,缓存内容100ms以内没有被使用,那么它就会被丢弃,无法复用
  • 离屏渲染内容有空间限制,超过屏幕2.5倍像素大小,也会失效,无法复用

被动触发

设置圆角触发的离屏渲染的离屏渲染。

cornerRadius.jpg

将半径设置为大于0.0的值会使该图层开始在其背景上绘制圆角。默认情况下,拐角半径不适用于图层的contents属性中的图像;它仅适用于图层的背景颜色和边框。但是,将masksToBounds属性设置为true会导致内容被裁剪到圆角。

此属性的默认值为0.0。

通过官方文档,我们可以知道设置cornerRadius仅适用于图层的背景颜色(backgroundColor)边框(border),而对其中的内容(content)无效,而如果要对内容设置圆角,则需要加上masksToBounds

clipsToBounds(UIView)是指视图上的子视图,如果超出父视图的部分就截取掉
masksToBounds(CALayer)却是指视图的图层上的子图层,如果超出父图层的部分就截取掉

我们来看一个例子。

    UIImageView *img = [[UIImageView alloc]init];
    img.frame = CGRectMake(150, 300, 100, 100);
    img.backgroundColor = [UIColor blueColor];
    [self.view addSubview:img];
    img.layer.cornerRadius = 50;
    img.image = [UIImage imageNamed:@"btn.png"];

看到结果,

非圆角.jpg

在上面的例子中我们可以看到,虽然设置了img.layer.cornerRadius = 50,但是仍然没有出现圆角:我们对img对象设置了圆角,但是cornerRadius对图片内容无效,所以就导致image里的内容仍然是正方形,盖在img上面后,出现了非圆角的情况。
接下里,我们加上

  img.layer.masksToBounds = YES;

可以看到效果


圆角.jpg

但是,这个时候会触发离屏渲染。因为我们本身背景色是一个图层,而内容又是一个图层,当两个图层叠加而且需要进行组合处理(画圆角)就会触发离屏渲染。

还记得上面那个例子吗?

    //1.按钮存在背景图片
    UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
    btn1.frame = CGRectMake(100, 30, 100, 100);
    btn1.layer.cornerRadius = 50;
    [self.view addSubview:btn1];
    btn1.layer.shouldRasterize = YES;
    [btn1 setImage:[UIImage imageNamed:@"btn.png"] forState:UIControlStateNormal];
    btn1.layer.masksToBounds = YES;

    //2.按钮不存在背景图片
    UIButton *btn2 = [UIButton buttonWithType:UIButtonTypeCustom];
    btn2.frame = CGRectMake(100, 180, 100, 100);
    btn2.layer.cornerRadius = 50;
    btn2.backgroundColor = [UIColor blueColor];
    [self.view addSubview:btn2];
    btn2.clipsToBounds = YES;

    //3.UIImageView 设置了图片+背景色;
    UIImageView *img1 = [[UIImageView alloc]init];
    img1.frame = CGRectMake(100, 320, 100, 100);
    img1.backgroundColor = [UIColor blueColor];
    [self.view addSubview:img1];
    img1.layer.cornerRadius = 50;
    img1.layer.masksToBounds = YES;
    img1.image = [UIImage imageNamed:@"btn.png"];

    //4.UIImageView 只设置了图片,无背景色;
    UIImageView *img2 = [[UIImageView alloc]init];
    img2.frame = CGRectMake(100, 480, 100, 100);
    [self.view addSubview:img2];
    img2.layer.cornerRadius = 50;
    img2.layer.masksToBounds = YES;
    img2.image = [UIImage imageNamed:@"btn.png"];
  • 为什么案例1会触发离屏渲染?因为UIButton本身有一个Layer,所以跟Image进行叠加画圆角,会导致离屏渲染。
  • 案例2,给UIButton添加背景色,此时只有一个图层,所以不会导致离屏渲染。
  • 案例3, UIImageView中本身的Layer和Content的Layer进行叠加,所以导致离屏渲染。
  • 案例4,同案例2。

防止

圆角

  • 祈求UI小姐姐给切一张图盖在上面,造成圆角的假象,但前提是要和UI小姐姐搞好关系。
  • 使用贝塞尔曲线UIBezierPath和Core Graphics框架画出一个圆角,需要注意的是Core Graphics通过CPU重新绘制一份带圆角的视图来实现圆角效果,会大大增加CPU的负担,而且相当于多了一份视图拷贝会增加内存开销。但是就显示性能而言,由于没有触发离屏渲染,所以能保持较高帧率。
  • 使用CAShapeLayer和UIBezierPath设置圆角,通过设置view.layer的mask属性,可以将另一个layer盖在view上,也可以设置圆角,但是mask同样会触发离屏渲染,但是对内存的消耗最少,而且渲染快速
-(void)pd_setRadius:(float)radius{
    
    UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:self.bounds byRoundingCorners:UIRectCornerAllCorners cornerRadii:CGSizeMake(radius, radius)];
    
    CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
    maskLayer.frame = bounds;
    maskLayer.path = maskPath.CGPath;
    
    [self.layer setMask: maskLayer];
}
  • CAShapeLayer继承于CALayer,可以使用CALayer的所有属性值;
  • CAShapeLayer需要贝塞尔曲线配合使用才有意义(也就是说才有效果)
  • 使用CAShapeLayer(属于CoreAnimation)与贝塞尔曲线可以实现不在view的drawRect(继承于CoreGraphics走的是CPU,消耗的性能较大)方法中画出一些想要的图形
  • CAShapeLayer动画渲染直接提交到手机的GPU当中,相较于view的drawRect方法使用CPU渲染而言,其效率极高,能大大优化内存使用情况。
  • 总的来说就是用CAShapeLayer的内存消耗少,渲染速度快,建议使用优化方案2。

阴影

设置阴影后,设置CALayer的 shadowPath。

let shadowView = UIView()
shadowView.frame = CGRect(x: 50, y: 100, width: 200, height: 200)
shadowView.layer.backgroundColor = UIColor.green.cgColor
shadowView.layer.shadowColor = UIColor.black.cgColor
shadowView.layer.shadowOpacity = 0.5
shadowView.layer.shadowRadius = 10
shadowView.layer.shadowOffset = CGSize(width: 10, height: 10)
shadowView.layer.shadowPath = UIBezierPath(rect: shadowView.bounds).cgPath
view.addSubview(shadowView)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349