关于 iOS 离屏渲染的分析与处理

一、OpenGL中,GPU屏幕渲染有以下两种方式

  • On-Screen Rendering 当前屏幕渲染: 是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行。


    没有离屏渲染时的步骤
  • Off-Screen Rendering 离屏渲染: GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。


    发生离屏渲染后的渲染步骤

二、离屏渲染定义:

通常情况下,我们在屏幕上显示都是读取帧缓冲区(Frame Buffer)渲染好的的数据,然后显示在屏幕上;而离屏渲染,则是将渲染好的图像放入离屏缓冲区(OffScreen Buffer)等多个图层的数据渲染完,再组合在一起,放入帧缓冲区,然后在屏幕上显示。


画家算法(把每一层依次输入到画布)

如上图,要在屏幕上显示图的ImageView,通常GPU的Render Server会遵循 “画家算法” 按秩序先渲染图1的那一层,然后渲染图2的那一层,最后渲染图3,渲染好后的每一层都会存入帧缓存区,然后按照次序绘制到屏幕,当绘制完一层,就会将该层从帧缓存区中移除(以节省空间)。

当对图3显示需要进行圆角和裁剪:imageView.clipsToBounds = YES,imageView.layer.cornerRadius=4.0时,渲染完图1,图2,图3,绘制到屏幕上后,还要进行裁减,无法在某一层渲染完成之后,再回过头来擦除/改变其中的某个部分,所以不能按照正常的流程,因此苹果会先渲染好每一层,存入一个缓冲区中,即离屏缓冲区,然后经过层叠加和处理后,再存储到帧缓存去中,然后绘制到屏幕上,这种处理方式叫做离屏渲染。

三、如何检测项目中哪些图层触发了离屏渲染问题:

  • 首先我们先开启离屏渲染的检测,在模拟器打开Color Off-screen Rendered,开启后会把那些需要离屏渲染的图层高亮成黄色,这就意味着黄色图层可能存在性能问题。
  //1.按钮存在背景图片
    UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
    btn1.frame = CGRectMake(100, 30, 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, 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和3变成了黄色,标记为触发了离屏渲染。

四、CALayer的层次结构及cornerRadius

  • CALayer由背景色backgroundColor、内容contents、边缘borderWidth&borderColor构成。


    CALayer的层次结构
  • cornerRadius


    cornerRadius:文档
  • 绘制图层背景圆角时使用的半径.可设置动画.

  • 设置layer.cornerRaclius只会设置backgroundColor的圆角.不会设置content的圆角,除非设置了layer.masksToBounds 为Ture(view中的clipsToBounds)

cornerRadius并不是对content(image)有效,根据苹果官方解释:cornerRadius 的文档中明确说明对 cornerRadius 的设置只对 CALayer 的 backgroundColor 和 borderWidth、borderColor起作用。
cornerRadius+masksToBounds 只有在设置了content且背景不是透明时,才会出现离屏渲染。
如果一定要使用cornerRadius+masksToBounds的方式裁切图片,不要设置backgroundColor。

五、离屏渲染有哪些问题

离屏渲染的代价很高,想要进行离屏渲染,首选要创建一个新的缓冲区,屏幕渲染会有一个上下文环境的一个概念,离屏渲染的整个过程需要切换上下文环境,先从当前屏幕切换到离屏,等结束后,又要将上下文环境切换回来。这也是为什么会消耗性能的原因了

  • 内存开支:开辟离屏缓冲区(大小不超过2.5倍屏幕像素大小)
  • 时间和性能开支:从离屏缓冲区拷贝数据到帧缓冲区,上下文切换耗性能

六、为什么要要使用离屏渲染

  • 用户需要特殊的渲染效果:使用额外的离屏缓冲区(offscreen butter)保存中间状态,最后叠加、处理后绘制在屏幕上,这样就不得不使用离屏渲染
  • 效率优势:需要多次使用的效果,提前渲染存入离屏缓冲区,然后复用来提高效率

七、解决标题三出现的问题

  • YY_image处理圆角的方法 - (UIImage *)yy_imageByRoundCornerRadius:(CGFloat)radius corners:(UIRectCorner)corners borderWidth:(CGFloat)borderWidth borderColor:(UIColor *)borderColor borderLineJoin:(CGLineJoin)borderLineJoin
为UIImage类扩展一个实例函数,仿YYImage做法
- (UIImage *)imageWithCornerRadius:(CGFloat)radius ofSize:(CGSize)size{
    /* 当前UIImage的可见绘制区域 */
    CGRect rect = (CGRect){0.f,0.f,size};
    /* 创建基于位图的上下文 */
    UIGraphicsBeginImageContextWithOptions(size, NO, UIScreen.mainScreen.scale);
    /* 在当前位图上下文添加圆角绘制路径 */
    CGContextAddPath(UIGraphicsGetCurrentContext(), [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:radius].CGPath);
    /* 当前绘制路径和原绘制路径相交得到最终裁剪绘制路径 */
    CGContextClip(UIGraphicsGetCurrentContext());
    /* 绘制 */
    [self drawInRect:rect];
    /* 取得裁剪后的image */
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    /* 关闭当前位图上下文 */
    UIGraphicsEndImageContext();
    return image;
}
  • Core Graphics方式 用贝塞尔曲线UIBezierPath和Core Graphics框架画出一个圆角
  • CAShapeLayer 方式

八、其他情况常见的触发离屏渲染以及解决办法:

  1. mask(遮罩)------>使用混合图层,在layer上方叠加相应mask形状的半透明layer

2.edge antialiasing(抗锯齿)----->不设置 allowsEdgeAntialiasing 属性为YES(默认为NO)

  1. allowsGroupOpacity(组不透明,开启CALayer的allowsGroupOpacity属性后,子 layer 在视觉上的透明度的上限是其父 layer 的opacity(对应UIView的alpha),并且从 iOS 7 以后默认全局开启了这个功能,这样做是为了让子视图与其容器视图保持同样的透明度。)------->关闭 allowsGroupOpacity 属性,按产品需求自己控制layer透明度

4.shadows(阴影)------>设置阴影后,设置CALayer的 shadowPath,view.layer.shadowPath=[UIBezierPath pathWithCGRect:view.bounds].CGPath;

CALayer离屏渲染终极解决方案:当视图内容是静态不变时,设置 shouldRasterize(光栅化)为YES(缓存离屏渲染的数据,当下次用到的时候直接拿,不需要开辟新的离屏缓冲区),此方案最为实用方便。view.layer.shouldRasterize = true;view.layer.rasterizationScale = view.layer.contentsScale;

shouldRasterize (光栅化使用建议):

1.如果layer不需要服用,则没有必要打开

2.如果layer不是静态的,需要被频繁修改,比如出于动画之中,则开启光栅化反而影响性能

3.离屏渲染缓存有时间限制,当超过100ms,内容没有被使用就会被丢弃,无法复用

4.离屏渲染缓存有空间限制,超过屏幕像素的2.5倍则失效,并无法使用

面试小结

在UITableVIewCell触发了离屏渲染,会导致在滑动的时候高频率的开辟离屏缓冲区,这样就会造成tableView滑动卡顿,如果视图内容是静态不变时,设置 shouldRasterize(光栅化)为YES,此方案最为实用方便,但是当视图内容是动态变化(如后台下载图片完毕后切换到主线程设置)时,使用此方案反而为增加系统负荷。

你可能感兴趣的:(关于 iOS 离屏渲染的分析与处理)