OpenGL-06-离屏渲染原理及触发条件

一、了解离屏渲染

1、正常渲染流程

APP -----> FrameBuffer(帧缓冲区) -----> Display

  • APP中的数据经过CPU和GPU计算渲染后,把结果放入帧缓冲区,再由视频控制器从甄嬛从去中读取并显示
  • GPU渲染过程中,显示到屏幕上的图像会遵循“画家算法”由远到近的顺序,依次将结果存储到帧缓冲区
  • 视频控制器从帧缓冲区读取一帧的数据将其显示后,就立刻丢弃了这一帧数据,然后进行下一帧的渲染显示。这样做的好处是节省了空间。


    image.png
2、离屏渲染流程及具体逻辑

APP -----> OffScreenBuffer(离屏缓冲区) -----> FrameBuffer(帧缓冲区) -----> Display

  • 当APP要进行额外的渲染和合并时(比如设置了圆角+裁剪),我们需要把不同的图层进行裁剪+合并的操作,这时就不能直接放入FrameBuffer了,我们要把渲染好的结果放入OffScreenBuffer,等待合适的机会将几个图层进行裁剪、合并叠加的操作,完成后把结果放入FrameBuffer中,由视频控制器读取显示
  • 离屏缓冲区相当于一个临时缓冲区,存放需要进行操作的数据,并不直接使用数据。因此,在方便我们的同时也有缺点,因为是额外开辟的空间,并且还需要转存数据到FrameBuffer中,所以大量的离屏渲染会影响性能,开销较大,也可能造成掉帧
  • OffScreenBuffer空间也是有限制的,是屏幕像素的2.5倍。如果缓存内容并100ms未被使用,会直接丢弃。


    image.png

二、离屏渲染触发的条件

我们通过代码调试来验证一下,通过打开模拟器的离屏选项来观察


image.png
1、高斯模糊 UIBlurEffectView(必定触发)
    //Button 背景色
    UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
    btn1.frame = CGRectMake(100, 50, 100, 100);
    btn1.backgroundColor = [UIColor redColor];
    [self.view addSubview:btn1];
    
    
    //Button 背景色+高斯模糊
    UIButton *btn2 = [UIButton buttonWithType:UIButtonTypeCustom];
    btn2.frame = CGRectMake(100, CGRectGetMaxY(btn1.frame)+50, 100, 100);
    btn2.backgroundColor = [UIColor redColor];
    [self.view addSubview:btn2];
    
    UIBlurEffect *effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
    UIVisualEffectView *effectVIew = [[UIVisualEffectView alloc]initWithEffect:effect];
    effectVIew.frame = btn2.bounds;
    [btn2 addSubview:effectVIew];
image.png

那么我们来看一下高斯模糊的离屏渲染逻辑


image.png
  • Content : 渲染内容
  • Capture Content : 捕获内容
  • Horizontal Blur : 水平模糊
  • Vertical Blur :垂直模糊
  • Compositing Pass : 合成过程
2、光栅化(必定触发)
    //Button 背景色
    UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
    btn1.frame = CGRectMake(100, 50, 100, 100);
    btn1.backgroundColor = [UIColor redColor];
    [self.view addSubview:btn1];
    
    
    //Button 背景色+光栅化
    UIButton *btn2 = [UIButton buttonWithType:UIButtonTypeCustom];
    btn2.frame = CGRectMake(100, CGRectGetMaxY(btn1.frame)+50, 100, 100);
    btn2.backgroundColor = [UIColor redColor];
    btn2.layer.shouldRasterize = YES;
    [self.view addSubview:btn2];
image.png

开启光栅化后,会触发离屏渲染,Render Server 会强制将 CALayer 的渲染位图结果 bitmap 保存下来,这样下次再需要渲染时就可以直接复用,从而提高效率。
而保存的 bitmap 包含 layer 的 subLayer、圆角、阴影、组透明度 group opacity 等,所以如果 layer 的构成包含上述几种元素,结构复杂且需要反复利用,那么就可以考虑打开光栅化。

使用光栅化shouldRasterize的一些建议:
1、如果layer不能被复用,没必要打开光栅化
2、如果layer是动态的,需要频繁修改,打开光栅化会造成很大的负荷,不建议打开
3、离屏缓冲区内容有时间限制,超过100ms没有被使用会被丢弃,无法复用
4、离屏缓冲区空间大小有限制,超过屏幕2.5倍就会失效,无法复用

3、阴影
    //Button 背景色
    UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
    btn1.frame = CGRectMake(100, 50, 100, 100);
    btn1.backgroundColor = [UIColor redColor];
    [self.view addSubview:btn1];


    //Button 背景色+阴影
    UIButton *btn2 = [UIButton buttonWithType:UIButtonTypeCustom];
    btn2.frame = CGRectMake(100, CGRectGetMaxY(btn1.frame)+50, 100, 100);
    btn2.backgroundColor = [UIColor redColor];
    [self.view addSubview:btn2];
    
    btn2.layer.shadowColor = UIColor.blackColor.CGColor;
    btn2.layer.shadowOffset = CGSizeMake(2, 2);
    btn2.layer.shadowOpacity = 0.9;

image.png

不过,阴影存在优化方案,就是指定一下阴影路径,就能解决了

//在上述代码的基础上添加
btn2.layer.shadowPath = [UIBezierPath bezierPathWithRect:btn2.bounds].CGPath;
4、圆角

我们先以UIButton和UIImageView为例,看不同条件下,圆角是否触发离屏渲染

   //针对UIButton的圆角分情况测试
    for (int i = 0; i < 5; i++) {
        UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
        btn.frame = CGRectMake(50, 150*i + 50, 100, 100);
        btn.layer.cornerRadius = 50;
        btn.clipsToBounds = YES;
        [self.view addSubview:btn];
        
        if (i == 0) {
            
            //背景色+边框+图片
            btn.backgroundColor = [UIColor redColor];
            btn.layer.borderWidth = 2;
            [btn setImage:[UIImage imageNamed:@"btn.png"] forState:UIControlStateNormal];
            
        }else if (i == 1){
            
            //背景色+边框
            btn.backgroundColor = [UIColor redColor];
            btn.layer.borderWidth = 2;
            
        }else if (i == 2){
            
            //背景色+图片
            btn.backgroundColor = [UIColor redColor];
            [btn setImage:[UIImage imageNamed:@"btn.png"] forState:UIControlStateNormal];
            
        }else if (i == 3){
            
            //边框+图片
            btn.layer.borderWidth = 2;
            [btn setImage:[UIImage imageNamed:@"btn.png"] forState:UIControlStateNormal];
            
        }else if (i == 4){
            
            //图片
            [btn setImage:[UIImage imageNamed:@"btn.png"] forState:UIControlStateNormal];
            
        }
    }
    
    
    //针对UIImageView的圆角分情况测试
    for (int j = 0; j < 5; j++) {
        
        UIImageView *img = [[UIImageView alloc]init];
        img.frame = CGRectMake(200, 150*j + 50, 100, 100);
        img.layer.cornerRadius = 50;
        img.layer.masksToBounds = YES;
        [self.view addSubview:img];
         
        if (j == 0) {
            
            //背景色+边框+图片
            img.backgroundColor = [UIColor redColor];
            img.layer.borderWidth = 2;
            img.image = [UIImage imageNamed:@"btn.png"];
            
        }else if (j == 1){
            
            //背景色+边框
            img.layer.borderWidth = 2;
            img.backgroundColor = [UIColor redColor];
            
        }else if (j == 2){
            
            //背景色+图片
            img.backgroundColor = [UIColor redColor];
            img.image = [UIImage imageNamed:@"btn.png"];
            
        }else if (j == 3){
            
            //边框+图片
            img.layer.borderWidth = 2;
            img.image = [UIImage imageNamed:@"btn.png"];
            
        }else if (j == 4){
            
            //图片
            img.image = [UIImage imageNamed:@"btn.png"];
            
        } 
        
    }
image.png

通过上图,打开离屏渲染的选项之后,可以看出10种测试,我们都设置了圆角+ clipsToBounds/masksToBounds,为什么有的触发了离屏渲染,有的没有?

首先,我们来结合CALayer的层级关系和cornerRadius的官方介绍分析一下:


image.png

image.png
  • CALayer由backgroundColor(背景颜色层)、contents(内容层)、border(边框属性层)构成。
  • 而cornerRadius的文档中明确说明:设置了cornerRadius,只对 CALayer 的backgroundColor和borderWidth&borderColor起作用,如果contents有内容或者内容的背景不是透明的话,只有设置masksToBounds为 true 才能起作用,此时两个属性相结合,产生离屏渲染。
  • 那么我们看代码中:
    1、针对UIButton,只要是 图片+ clipsToBounds(即masksToBounds)的情况,都会触发离屏渲染
    2、针对UIImageView,只有 图片+背景色/边框+ masksToBounds,才会触发离屏渲染
    【这里我们要看一下iOS官方针对UIImageView做的一些优化:
    1、在iOS9之前,UIImageView和UIButton通过cornerRadius+masksToBounds/clipsToBounds设置圆角都会触发离屏渲染,
    2、在iOS9以后,针对UIImageView中的image设置圆角并不会触发离屏渲染,如果加上了背景色或者阴影等其他效果还是会触发离屏渲染的】

这样我们就解释的通了。

那么我们这里的contents仅仅指的是图片吗?

其实并不是,于是笔者尝试了以下代码,总结出,contents也可以是有色信息(颜色、图片)的子视图

for (int i = 0; i < 3; i++) {
        
        
       UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
       btn.frame = CGRectMake(50, 150*i + 50, 100, 100);
       btn.backgroundColor = [UIColor redColor];
       btn.layer.cornerRadius = 50;
       btn.clipsToBounds = YES;
       [self.view addSubview:btn];
       
       if (i == 0) {
           //无颜色
           UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
           btn1.frame = CGRectMake(0, 0 , 50, 50);
           btn1.backgroundColor = [UIColor clearColor];
           [btn addSubview:btn1];
           
       }else if (i == 1){
           //有颜色
           UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
           btn1.frame = CGRectMake(0, 0 , 50, 50);
           btn1.backgroundColor = [UIColor blackColor];
           [btn addSubview:btn1];
           
       }else if (i == 2){
           //有图片
           UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
           btn1.frame = CGRectMake(0, 0 , 50, 50);
           [btn1 setImage:[UIImage imageNamed:@"btn.png"] forState:UIControlStateNormal];
           [btn addSubview:btn1];
           
       }

image.png

所以,针对圆角如何避免触发离屏渲染,我们可以根据上述条件,根据自身项目需求进行特殊定制

5、layer.mask (遮罩/蒙版)

我们来看一下mask的渲染逻辑


image.png

如图:

  • 系统先计算好mask部分,然后保存到离屏缓冲区
  • 计算layer部分,计算好之后保存到离屏缓冲区
  • 对mask和layer进行合并剪裁计算,最后结果提交到FrameBuffer,展示到屏幕上

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

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

6、组透明度(layer.allowsGroupOpacity / layer.opacity)

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

另外,两个半透明的view,通过addSubView方法叠加,也会产生离屏渲染。

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

那么

总结一下,常见的触发情况
1、使用了 mask 的 layer (layer.mask)
2、需要进行裁剪的 layer (layer.masksToBounds / view.clipsToBounds) ,同时拥有多层layer需要处理的情况
3、设置了组透明度为 YES,并且透明度不为 1 的 layer(layer.allowsGroupOpacity/layer.opacity)
4、添加了阴影 (layer.shadow)
5、采用了光栅化 (layer.shouldRasterize)
6、绘制了文字的 layer (UILabel, CATextLayer, Core Text 等)
7、使用了高斯模糊
8、使用了抗锯齿(edge antialiasing)【allowsEdgeAntialiasing = YES】

三、离屏渲染与性能优化

1、离屏渲染的好处
  • 为了特殊效果,不得不使用。例如系统自动触发的情况:圆角、阴影、高斯模糊、光栅化
  • 提升效率。如果一个效果需要多次用到,我们可以提前渲染保存在offscreenbuffer中,免去重复计算的时间,达到复用的目的。这需要手动触发
2、 如何避免离屏渲染做到性能优化
  • 圆角:虽然并不是所有的圆角+裁剪都会触发,但是我们也要分情况使用,可以使用切好的圆角图片,或者自己使用贝塞尔曲线进行圆角绘制
  • 透明度:多层级的视图添加,不要设置透明度;不要设置组透明度
  • 光栅化:当不存在短时间内需要反复多次大量复用的layer时,shouldRasterize设置为NO
  • 阴影:增加阴影路径
  • mask:使用混合图层,在layer上方叠加相应mask形状的半透明layer
  • 抗锯齿:不开启 allowsEdgeAntialiasing 属性 (默认为NO)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342