OpenGL - 离屏渲染

2,044 阅读7分钟

参考链接:

关于iOS离屏渲染的深入研究

ios渲染原理

一、定义

iOS页面显示流程是,CPU将需要显示的内容进行处理,例如顶点、图元、颜色等信息,处理完成后交给GPU进行渲染,渲染后放在一个和屏幕像素数据量一样大的缓存空间frame buffer里面,这也就是GPU存储渲染结果的地方。

但是有时候有多个图层,当对下面图层做一些处理,例如圆角,并且要求上面子图层需要跟父图层做一样的处理,这个时候就需要将每个图层的渲染结果存储在另外的内存区域offscreenBuffer,然后所有图层都渲染完成后,作为处理后再将结果写入到frame buffer里面,这个过程就被称之为离屏渲染。

二、离屏渲染的检测方式

  • 可以通过在模拟器上,Debug-> Color Off-Screen Rendered
  • 其中出现黄色背景的,则为触发了离屏渲染

三、离屏渲染的触发方式

1、毛玻璃以及shouldRasterize光栅化

a、代码

 CGFloat y = 60;
     // 触发方式1: 毛玻璃效果
    UIButton *btn0 = [UIButton buttonWithType:UIButtonTypeCustom];
    btn0.frame = CGRectMake(0, y, 50, 50);
    [self.view addSubview:btn0];
    [btn0 setImage:[UIImage imageNamed:@"BYLaunchPrivacyImage"] forState:UIControlStateNormal];
    UIBlurEffect *effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
    UIVisualEffectView *effectView = [[UIVisualEffectView alloc] initWithEffect:effect];
    //必须给effcetView的frame赋值,因为UIVisualEffectView是一个加到UIIamgeView上的子视图.
    effectView.frame = CGRectMake(0, 0, 30, 30);
    [btn0 addSubview:effectView];

    // 触发方式2: shouldRasterize
    UIButton *btn_1 = [UIButton buttonWithType:UIButtonTypeCustom];
    btn_1.frame = CGRectMake(100, y, 50, 50);
    btn_1.layer.shouldRasterize = YES;
    [btn_1 setImage:[UIImage imageNamed:@"BYLaunchPrivacyImage"] forState:UIControlStateNormal];
    [self.view addSubview:btn_1];

    // 触发方式2: shouldRasterize
    UIButton *btn_2 = [UIButton buttonWithType:UIButtonTypeCustom];
    btn_2.frame = CGRectMake(200, y, 50, 50);
    [btn_2 setImage:[UIImage imageNamed:@"BYLaunchPrivacyImage"] forState:UIControlStateNormal];
    [self.view addSubview:btn_2];

b、效果

我们发现:

  • 当我们仅仅给btn设置image的话是没有离屏渲染的,说明当我们给btn设置image时候相当于在btn上面加了一个imageView,这时候就相当btn上面有两个layer
  • 当我们给btn加上毛玻璃效果时候,毛玻璃效果地方出现了离屏渲染
  • 当我们设置了shouldRasterize时候,btn也出现了离屏渲染

2、圆角+maskToBounds

a、代码

// 触发方式3: 圆角+maskToBounds
    //UIImageView 设置了图片+背景色;
      UIImageView *img1 = [[UIImageView alloc]init];
      img1.frame = CGRectMake(100, 200, 50, 50);
      img1.backgroundColor = [UIColor blueColor];
      [self.view addSubview:img1];
      img1.layer.cornerRadius = 10;
      img1.layer.masksToBounds = YES;
      img1.image = [UIImage imageNamed:@"BMC_Common_EditBtn"];

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

    //UIImageView 只设置了图片,无背景色;
    UIImageView *img3 = [[UIImageView alloc]init];
    img3.frame = CGRectMake(100, 320, 50, 50);
    img3.backgroundColor = [UIColor lightGrayColor];

    [self.view addSubview:img3];
    img3.layer.cornerRadius = 10;
    img3.image = [UIImage imageNamed:@"BMC_Common_EditBtn"];

b、效果

我们发现:

  • 当设置了背景颜色和image的同时,再加上cornerRadius和masksToBounds,会触发离屏渲染
  • 如果只设置了image,加上加上cornerRadius和masksToBounds不会触发离屏渲染,根据第一条,说明当设置了backgroundColor和image,imageView拥有了两个图层,其中background一个layer,image一个layer,其实每个layer就是一个图层
  • 假如设置了背景颜色和image,如果只有cornerRadius,但是没有masksToBounds,不会触发离屏渲染,这是因为没设置masksToBounds,那圆角只是针对imageView一个图层有效,对于其他图层无用

3、父view和子view都有透明度

a、代码

//4.父view和子view都有透明度;
     UIView *v1 = [[UIView alloc]init];
     v1.frame = CGRectMake(50, 500, 100, 100);
     v1.layer.cornerRadius = 50;
     v1.backgroundColor = [UIColor colorWithRed:1 green:0.3 blue:0.2 alpha:1];
     v1.alpha = 0.5;
     [self.view addSubview:v1];

     UIView *v2 = [[UIView alloc]init];
     v2.frame = CGRectMake(30, 30, 50, 50);
     v2.layer.cornerRadius = 20;
     v2.backgroundColor = [UIColor colorWithRed:0.3 green:1 blue:0.2 alpha:1];
     v2.alpha = 0.5;
     [v1 addSubview:v2];

b、效果

p1-jj.byteimg.com/tos-cn-i-t2…

我们发现触发了离屏渲染,但是假如父视图有两个子view。并且两个子view都有透明度且叠加在一起,这样不会触发离屏渲染

三、离屏渲染的原理

当有多个图层叠加在一起的时候,绘制一帧图形的时候会先绘制场景中离观察者较远的物体,在绘制较近的物体,例如下图:

先绘制红色部分,再绘制⻩色部分,最后再绘制灰⾊部分,即可解决隐藏面消除的问题。即将场景按照物理距离和观察者的距离远近排序,由远及近的绘制即可,这个时候就是普通渲染

但是当我们设置了cornerRadius和masksToBounds进行圆角+裁剪时,masksToBounds裁剪属性会应用到所有的图层上。

本来我们从后往前绘制时候,绘制完一个图层就可以将该图层丢弃了,但现在需要依次在 Offscreen Buffer中保存,等待圆角+裁剪处理,即引发了 离屏渲染

但是根据文档我们知道:

  • 背景色、边框、背景色+边框,再加上圆角+裁剪,根据文档说明,因为 contents = nil 没有需要裁剪处理的内容,所以masksToBounds设置为YES或者NO都没有影响。

  • 一旦我们 为contents设置了内容 ,无论是图片、绘制内容、有图像信息的子视图等,再加上圆角+裁剪,就会触发离屏渲染。

也就是说如果只有一个layer,那么无论masksToBounds设置为YES或者NO都不会产生离屏渲染,但是毛玻璃效果本身是在原view上面加一个子view,所以不是一个layer

1、普通渲染

  • 普通渲染:App -> FrameBuffer -> Display
  • GPU将每一个subLayer绘制完成后就直接把subLayer丢弃,丢弃了就不能再对绘制完的图案就行处理了
  • 当所有subLayer都绘制完了后,就形成了一帧屏幕,然后将该帧屏幕缓存到frameBuffer中,等屏幕去显示

2、离屏渲染

  • 离屏渲染:App -> Off-Screen Buffer(进行计算合并等操作) -> FrameBuffer -> Display
  • 因为触发离屏渲染是因为需要等所有的subLayer绘制完后再进行mask或者cornerRadius等处理,处理后再缓存到frameBuffer中
  • 所有每个subLayer绘制完后需要缓存到offscreen中,等所有subLayer绘制完成后,再对offscreen里面的subLayer进行组合处理,处理完成后放到frameBuffer中

3、离屏渲染流程

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

四、离屏渲染的性能优化

1、 需要优化的原因

  • 离屏渲染需要单独开辟一个缓存区域offscreenBuffer
  • 将sublayer缓存到offscreenBugger需要额外的存储时间
  • offscreenBuffer的缓存的空间有限制,大小在屏幕像素的2.5倍
  • 一旦缓存超过100ms没有被使用,会自动被丢弃
  • 额外的处理会造成CPU性能的损耗

GPU的操作是高度流水线化的。本来所有计算工作都在有条不紊地正在向frame buffer输出,此时突然收到指令,需要输出到另一块内存,那么流水线中正在进行的一切都不得不被丢弃,切换到只能服务于我们当前的“切圆角”操作。等到完成以后再次清空,再回到向frame buffer输出的正常流程。

在tableView或者collectionView中,滚动的每一帧变化都会触发每个cell的重新绘制,因此一旦存在离屏渲染,上面提到的上下文切换就会每秒发生60次,并且很可能每一帧有几十张的图片要求这么做,对于GPU的性能冲击可想而知(GPU非常擅长大规模并行计算,但是我想频繁的上下文切换显然不在其设计考量之中)

2、优化的方案

尽管离屏渲染开销很大,但是当我们无法避免它的时候,可以想办法把性能影响降到最低,尽量只在下面几种情况触发:

  • 1、没法一次性画出效果
  • 2、特殊效果:需要使用额外的offscreenBuffer保存中间状态,不能不使用,系统自动触发,圆角、阴影
  • 3、效率的优势:既然效果会多次屏幕,提前渲染结果保存在offscreenBuffer中,然后复用目的
  • 4、对于view的圆形边框,如果没有backgroundColor,可以放心使用cornerRadius来做
  • 5、对于特殊形状的view,使用layer mask并打开shouldRasterize来对渲染结果进行缓存
  • 6、对于模糊效果,不采用系统提供的UIVisualEffect,而是另外实现模糊效果(CIGaussianBlur),并手动管理渲染结果