离屏渲染:

 

前面分析我们知道,iOS的渲染流程:
主要流程:Core Animation 提交会话,包括自己和子树的layout 状态等(CPU进行解压计算等操作)
RenderServer 解析提交的子树状态,生成绘制指令
GPU 执行绘制指令
显示渲染后的数据

 

详细代码参见Demo  离屏渲染

Demo地址 -> iOSDemos -> iOSOpenGLDemos

 

1、离屏渲染

简单的可以理解为一帧的画面是有多个渲染图层合并的结果,在这些图层合并到帧缓冲区之前,需要一个一个图层进行渲染,存放到离屏缓冲区中。全部渲染完毕然后合并到帧缓冲区,这些不能一步画出来结果的渲染就是是离屏渲染。

 

正常渲染流程

渲染的流程是经过CPU计算,然后给到GPU进行渲染,将渲染的结果存到帧缓冲区。视图控制器会去帧缓冲区读取数据,然后经过数模装换再逐行显示到屏幕上。

android opengl渲染图片 手机opengl渲染_离屏渲染

离屏渲染流程

给到GPU 进行渲染的时候,先渲染出来的内容,放到offscreen Buffer 离屏缓冲区中,等到全部渲染完成,然后合并放到帧缓冲区然后显示。

android opengl渲染图片 手机opengl渲染_圆角_02

 

在GPU进行渲染的时候,一般情况下是遵循“画家算法”按纵深层次由远及近的一层一层将视图渲染出来,然后结果放到帧缓冲区。当前帧缓冲区的数据一旦显示到屏幕上之后,该帧就会丢弃,重新渲染并存放新一帧数据。

android opengl渲染图片 手机opengl渲染_ci_03

离屏渲染与普通情况下GPU 直接将渲染好的内容放入Frame Buffer 不同,需要先额外创建离屏渲染缓冲区offscreen Buffer保存中间状态,将提前渲染好的内容放入其中,等需要的全部图层全部渲染完之后通过合并,存放到帧缓冲区,等待读取显示。

例如遮罩(Making)
1、APP提交到Core Animation
2、Core Animation 将事件提交到 Render Server(渲染服务)(OpenGL ES / Metal 去处理)
Vertex Shader 顶点着色器,确定显示在什么位置
Tiling 片段(类似光栅化,就是标记出来那些像素点,依次往里填充颜色)
Pixel Shader 片元着色器(像素着色器)
经过以上处理之后,拿到的是图层(1),保存在提前创建的离屏缓冲区(Mask Texture)中。

然后依次进行 图层(2)的处理,最后将(1)(2)进行合并。然后渲染出来,放到帧缓冲区,一次性显示在界面上

android opengl渲染图片 手机opengl渲染_ci_04

 

离屏渲染会有性能问题

1、需要额外的存储空间
2、从offscreen Buffer 转存到 Frame Buffer 也是需要消耗时间的
3、由于需要全部渲染完之后再进行合成显示,在垂直信号16.7ms内完成才能不卡顿掉帧。所以相对于不使用离屏渲染,更容易卡顿掉帧。
4、Offscreen Buffer 是有空间限制的,最大是屏幕的宽*高*4(RGBA)的2.5倍。如果超过限制大小会无法渲染

使用离屏渲染的目的:

1、特殊效果,一次无法完全渲染出来显示到屏幕上,必须要经过几次渲染合并的需要使用 offscreen Buffer 来保存中间状态。由系统自动触发
2、可以复用的渲染,提交效率。有些效果会多次渲染到屏幕上的时候,可以提前渲染offscreen Buffer 用于重复使用。手动触发

特殊效果,系统自动触发的。如上面说到的遮罩、阴影、圆角(有部分情况不会触发,后面介绍)等

手动触发:毛玻璃(UIVisualEffectView)
1、先通过顶点着色器、片元着色器等渲染原图
2、如果有缩放,处理缩放后的结果
3、对上面的结果进行垂直毛玻璃效果的添加渲染
4、对上面的结果进行水平的毛玻璃效果的渲染
5、最后将结果合成并显示

android opengl渲染图片 手机opengl渲染_ci_05

 

android opengl渲染图片 手机opengl渲染_圆角_06

 

2、触发离屏渲染的几种情况

1、使用 mask(遮罩)的 layer (layer.mask)
2、需要进行裁剪的 layer (layer.masksToBounds / view.clipsToBounds)
3、设置了组透明度为 YES ,并且透明度不为 1 的 layer (layer.allowsGroupOpacity / layer.opacity)
4、添加了投影的 layer (layer.shadow *)
5、采用了光栅化的 layer(layer.shouldRasterize)
6、绘制了文字的 layer (UILabel, CATextLayer, Core Text 等)

 

3、离屏渲染 光栅化(shouldRasterize)使用建议:

如果layer 不能被复用,则没有必要打开光栅化
如果layer 不是静态的,需要被频繁修改,比如处于动画之中,那么开启离屏渲染反而影响效率了。
离屏渲染缓存内容有时间限制,缓存内容在100ms内如果没有被使用的话,那么就会被丢弃,无法在再复用(需要的时候在重新渲染)。
离屏渲染缓存空间有大小限制(上面说了是屏幕宽*高*4(RGBA)的2.5倍),超过2.5倍屏幕大小的像素的话,会失效无法复用。

圆角触发离屏渲染
通常说设置了layer 的圆角会触发离屏渲染,其实并不然。限制就来分析一下。
如下图,最上一层是边框,然后是内容,最后是背景颜色。

android opengl渲染图片 手机opengl渲染_圆角_07

在这个图中 layer 由三层,如果设置圆角
view.layer.cornerRadius = 10;

Setting the radius to a value greater than 0.0 causes the layer to begin drawing rounded corners on its background. By default, the corner radius does not apply to the image in the layer’s contents property; it applies only to the background color and border of the layer. However, setting the masksToBounds property to true causes the content to be clipped to the rounded corners.

通过Apple 文档 可知,只是默认设置backgroundColor 和 border 的圆角,并不会设置 content 的圆角,除非同时设置了 layer.masksToBounds  为 true (对应UIView 中的 clipToBounds 属性);
总结:当作用到所有layer (layer 及 subLayer)上的时候就会触发离屏渲染。

CGFloat w = SCREEN_Width/4.0f;
    CGFloat h = (SCREEN_Height-Height_NavStatusBar)/6.0f;
    CGFloat spacingH = h/2.f;
    CGFloat spacingW = w/2.f;
    CGFloat top = Height_NavStatusBar+spacingH;
    CGFloat left1 = spacingW;
    CGFloat left2 = spacingW+SCREEN_Width/2.f;
    
    UIImageView * imageView1 = [[UIImageView alloc] initWithFrame:CGRectMake(left1, top, w, h)];
    imageView1.image = [UIImage imageNamed:@"file_rar"];
    imageView1.layer.cornerRadius = 10;
    imageView1.backgroundColor = [UIColor grayColor];
    imageView1.contentMode = UIViewContentModeScaleAspectFit;
    [self.view addSubview:imageView1];
    
    
    UIImageView * imageView2 = [[UIImageView alloc] initWithFrame:CGRectMake(left2, top, w, h)];
    imageView2.image = [UIImage imageNamed:@"file_rar"];
    imageView2.layer.cornerRadius = 10;
    imageView2.backgroundColor = [UIColor grayColor];
    imageView2.contentMode = UIViewContentModeScaleAspectFit;
    imageView2.layer.masksToBounds = YES;
    [self.view addSubview:imageView2];
    
    
    CGFloat top1 = top + h + spacingH*2;
    
    //1.按钮存在背景图片
    UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
    btn1.frame = CGRectMake(left1, top1, w, h);
    btn1.layer.cornerRadius = 50;
    [self.view addSubview:btn1];
    
    [btn1 setImage:[UIImage imageNamed:@"file_rar"] forState:UIControlStateNormal];
    btn1.clipsToBounds = YES;
    
    //2.按钮不存在背景图片
    UIButton *btn2 = [UIButton buttonWithType:UIButtonTypeCustom];
    btn2.frame = CGRectMake(left2, top1, w, h);
    btn2.layer.cornerRadius = 50;
    btn2.backgroundColor = [UIColor blueColor];
    [self.view addSubview:btn2];
    btn2.clipsToBounds = YES;
    
    
    CGFloat top2 = top1 + h + spacingH*2;
    
    //3.UIImageView 设置了图片+背景色;
    UIImageView *img1 = [[UIImageView alloc]init];
    img1.frame = CGRectMake(left1, top2, w, h);
    img1.backgroundColor = [UIColor blueColor];
    [self.view addSubview:img1];
    img1.layer.cornerRadius = 50;
    img1.layer.masksToBounds = YES;
    img1.image = [UIImage imageNamed:@"file_rar"];
    
    //4.UIImageView 只设置了图片,无背景色;
    UIImageView *img2 = [[UIImageView alloc]init];
    img2.frame = CGRectMake(left2, top2, w, h);
    [self.view addSubview:img2];
    img2.layer.cornerRadius = 50;
    img2.layer.masksToBounds = YES;
    img2.image = [UIImage imageNamed:@"file_rar"];

2. 离屏渲染的检测

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

android opengl渲染图片 手机opengl渲染_离屏渲染_08

 

结果

 

android opengl渲染图片 手机opengl渲染_圆角_09

如上代码和效果可以验证,不使用 layer.masksToBounds(clipToBounds)的时候,不会去触发离屏渲染。用的时候去设置圆角才会触发离屏渲染。
如上分析中,知道图层的显示遵循“画家算法”由远及近的一层一层叠加而成。
普通渲染的时候,是图层的叠加。渲染完的subLayer 放到帧缓存区,系统从帧缓存区取出显示到屏幕上。一旦subLayer 绘制到屏幕上了之后,这个subLayer就会从帧缓存区中移除(节省空间)

android opengl渲染图片 手机opengl渲染_离屏渲染_10

圆角+裁剪(cornerRadius + masksToBounds)
离屏渲染,离屏渲染是将三个layer 分别渲染并设置圆角,最后将3个layer 组合成一个,去显示Display

android opengl渲染图片 手机opengl渲染_android opengl渲染图片_11

android opengl渲染图片 手机opengl渲染_圆角_12

触发离屏渲染的几种组合

  • 圆角+裁剪(cornerRadius + masksToBounds)
  • 透明度+组透明(layer.allowsGroupOpacity+layer.opacity)
  • 阴影属性(shadowOffset )

等。因为组透明度、阴影都是和裁剪类似的,会作用与 layer 以及其所有 sublayer 上,这就导致必然会引起离屏渲染。

 

如何减少乃至避免圆角的离屏渲染

layer.masksToBounds 作用到所有layer 触发的离屏渲染,那么就不去使用layer.masksToBounds。 从Apple的文档可以知道,不设置 layer.masksToBounds 的时候,content 是不会被设置圆角的,那么最好的办法就是直接换一个带圆角的图片进来。

重写 drawRect: 方法并不会触发离屏渲染。前文中我们提到过,重写 drawRect: 会将 GPU 中的渲染操作转移到 CPU 中完成,并且需要额外开辟内存空间。但根据苹果工程师的说法,这和标准意义上的离屏渲染并不一样,在 Instrument 中开启 Color offscreen rendered yellow 调试时也会发现这并不会被判断为离屏渲染。

 

网上也提供的有其他的方法,我认为最好的办法还是去直接换张图,这里就不多说了,仅仅摘抄了YYimage 的圆角处理方法。其他的方法文章最后有参考的文章地址,里面有提及可自行查阅。

转载网上的一些方法如下:

方案一:

_imageView.clipsToBounds = YES;

_imageView.layer.cornerRadius = 4.0f;

方案二:

android opengl渲染图片 手机opengl渲染_ci_13

方案三:

android opengl渲染图片 手机opengl渲染_android opengl渲染图片_14

方案四:

android opengl渲染图片 手机opengl渲染_ci_15

 

YYImage 圆角处理方法

- (UIImage *)yy_imageByRoundCornerRadius:(CGFloat)radius
 corners:(UIRectCorner)corners
 borderWidth:(CGFloat)borderWidth
 borderColor:(UIColor *)borderColor
 borderLineJoin:(CGLineJoin)borderLineJoin {
     if (corners != UIRectCornerAllCorners) {
         UIRectCorner tmp = 0;
         if (corners & UIRectCornerTopLeft) tmp |= UIRectCornerBottomLeft;
         if (corners & UIRectCornerTopRight) tmp |= UIRectCornerBottomRight;
         if (corners & UIRectCornerBottomLeft) tmp |= UIRectCornerTopLeft;
         if (corners & UIRectCornerBottomRight) tmp |= UIRectCornerTopRight;
         corners = tmp;
     }
     UIGraphicsBeginImageContextWithOptions(self.size, NO, self.scale);
     CGContextRef context = UIGraphicsGetCurrentContext();
     CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height);
     CGContextScaleCTM(context, 1, -1);
     CGContextTranslateCTM(context, 0, -rect.size.height);
     CGFloat minSize = MIN(self.size.width, self.size.height);
     if (borderWidth < minSize / 2) {
         UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(rect, borderWidth, borderWidth) byRoundingCorners:corners
        cornerRadii:CGSizeMake(radius, borderWidth)];
         [path closePath];
         CGContextSaveGState(context);
         [path addClip];
         CGContextDrawImage(context, rect, self.CGImage);
         CGContextRestoreGState(context);
     }
     if (borderColor && borderWidth < minSize / 2 && borderWidth > 0) {
         CGFloat strokeInset = (floor(borderWidth * self.scale) + 0.5) / self.scale;
         CGRect strokeRect = CGRectInset(rect, strokeInset, strokeInset);
         CGFloat strokeRadius = radius > self.scale / 2 ? radius - self.scale / 2 : 0;
         UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:strokeRect byRoundingCorners:corners cornerRadii:CGSizeMake(strokeRadius,
        borderWidth)];
         [path closePath];
         path.lineWidth = borderWidth;
         path.lineJoinStyle = borderLineJoin;
         [borderColor setStroke];
    }
}

 

参阅:

1、深入iOS离屏渲染

2、iOS 渲染原理解析

3、离屏渲染的触发原理&&躲在背后的性能优化4、iOS离屏渲染探索5、四、深入剖析【离屏渲染】原理6、iOS中的离屏渲染相关