OpenGL入门 -- iOS 离屏渲染解读

542 阅读6分钟

离屏渲染问题是在面试中经常会被问到的一个问题,都知道设置layer圆角,会触发离屏渲染,那么只有设置圆角就会触发离屏渲染吗?

那么,我们来写一段代码测试一下,是否是所有的圆角都会触发离屏渲染

我们定义几个不同情况的按钮,设置圆角,测试代码如下:

    //1.按钮存在背景图片
    UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
    btn1.frame = CGRectMake(100, 80, 100, 100);
    btn1.layer.cornerRadius = 50;
    [self.view addSubview:btn1];
    
    [btn1 setImage:[UIImage imageNamed:@"btn.png"] forState:UIControlStateNormal];
    btn1.clipsToBounds = YES;
    
    //2.按钮不存在背景图片
    UIButton *btn2 = [UIButton buttonWithType:UIButtonTypeCustom];
    btn2.frame = CGRectMake(100, 200, 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, 340, 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, 520, 100, 100);
    [self.view addSubview:img2];
    img2.layer.cornerRadius = 50;
    img2.layer.masksToBounds = YES;
    img2.image = [UIImage imageNamed:@"btn.png"];

当开启离屏渲染时,如上对比图,发现并不是所有的圆角都触发了离屏渲染。

接下来分析一下离屏渲染触发的原因。

离屏渲染触发的原因

屏幕显示数据有两种加载流程:

  • 正常渲染加载流程
  • 离屏渲染加载流程

这两种加载流程简单示意图如下:

接下来仔细说一下这两种加载流程

  • 正常渲染流程

    正常的APP加载是数据经过CPUGPU的计算后,存储在帧缓冲区 Frame Buffer中,屏幕不断从帧缓冲区读取数据显示到屏幕上的。

    GPU渲染流程中,显示到屏幕上的图像遵循画家算法,按照从远到近的原则,依次将结果存储到帧缓冲区,视频控制器从Frame Buffer中,读取一帧数据后,显示到屏幕上,然后立即丢弃这一帧数据,不做任何保留,这样做的目的是节省空间,且在屏幕上各自显示,互不影响。

  • 离屏渲染流程

    当需要渲染特殊效果时(比如:圆角、毛玻璃、高斯模糊、阴影等),不能一次性得到渲染结果,这时,就需要将渲染处理好的数据不直接放到Frame Buffer中,而是放到离屏缓冲区 offScreen Buffer中,最后把几个图层的结果叠加之后,放到Frame buffer中,然后显示到屏幕上。

离屏缓冲区 offScreen Buffer是额外开辟的一个缓冲区,也是有空间限制,其大小为屏幕像素点的2.5倍,其开销也是很大的,那为什么还要用离屏渲染呢?

  • 特殊效果,需要使用offscreen buffer保存中间状态,不得不使用离屏渲染,是系统自动触发,比如: 遮罩,圆角,高斯模糊,阴影,毛玻璃效果
  • 效率优势,既然效果要多次出现在屏幕上,可以提前渲染,保存到offscreen buffer中,达到复用的目的。是手动触发,以达到复用目的、 抗锯齿目的。

接下来以遮罩为例,分析一下,为什么需要离屏渲染以及渲染的流程。

如上图,想要实现图片③的效果,需要把图片②覆盖在图片①上,最终渲染成图③的样子,用正常的渲染流程,从帧缓冲区读取一帧数据,显示,然后丢弃,无法到达这种遮罩的效果,所以需要将渲染好的图层,存放到离屏缓冲区 offscreen buffer中,等全部图层都处理完毕后,叠加混合,存到帧缓冲区中,然后视频控制器读取帧缓冲区中数据,显示到屏幕上。

详细流程如下:

毛玻璃效果的流程和遮罩类型,不在详细描述,流程如下:

触发离屏渲染的另一个原因--光栅化 shouldRasterize

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

当我开启光栅化,设置shouldRasterizeYES时,会将layer渲染成的位图保存在offscreen buffer中,下次直接使用,提供效率。

光栅化使用建议:

  • 如果layer不能复用,则没有必要打开光栅化
  • 如果layer不是静态的,需要频繁被渲染,比如存在于动画中,那么开了光栅化,离屏渲染反而影响了效率
  • 离屏渲染缓存内容有时间限制,缓存内容100ms内,没有被使用,那么会被丢弃,无法再使用
  • 离屏渲染缓存空间有限,超过屏幕像素的2.5倍,就会失效,且无法被复用

圆角中的离屏渲染

首先看一下CALayer的构成,如下图:是有backgroundColorcontentsborderWidth borderColor构成,设置圆角,为什么会触发离屏渲染的原因,就在这里。

在设置圆角的时候,我们都知道,在设置完cornerRadiusborderWidthborderColor等属性后,我们还要设置masksToBoundsYES才会生效,起原理是大部人都没有去仔细研究的。

下面是苹果官方文档针对圆角设置的一些说明:

从文档中得知:在设置了cornerRadius,只会对backgroundColorboder设置圆角,不会设置contents的圆角,除非同时设置了maskToBounds / clipsToBounds属性。

也可以理解为:圆角不生效的根本原因是没有对contents设置圆角,而设置的image是放在contents里面的,所以看到的界面上的就是image没有进行圆角裁剪。这也验证了开篇的那个一段测试代码。

小结:

  • 当只设置backgroundColorborder,而contents中没有子视图时(可以简单的理解为没有设置图片),无论maskToBounds / clipsToBoundstrue还是false,都不会触发离屏渲染
  • contents中有子视图时,此时设置 cornerRadius + maskToBounds / clipsToBounds,就会触发离屏渲染,但是这种情况在UIImageView中并不适用
  • UIImageView中只设置图片和maskToBounds / clipsToBounds是不会触发离屏渲染(开篇有例子),苹果对UIImageView优化猜测是只是将image直接画在了contents上面,这样不设置背景色其实只需要渲染一个layer,所以不需要用到离屏缓冲区,也就不会触发离屏渲染,如果此时再加上背景色,就会触发离屏渲染。

常见触发离屏渲染的几种情况:

  • 使用了masklayer (layer.mask)
  • 需要进行裁剪的layermaskToBounds / clipsToBounds
  • 设置了透明度为yes,并且透明度不为1
  • 添加了投影的layer,(layer.shadow
  • 采用了光栅的layer,(layer.shouldRasterize
  • 绘制了文字的layer,(UILabel, CATextLayer, Core Text