Hello OpenGL--003:离屏渲染

一、画面撕裂

1.1画面撕裂的形成

在介绍离屏渲染之前我们先了解一下什么是画面撕裂,以及其形成的原因:

画面撕裂

在游戏中我们有时会遇到这样的画面,我们很明显的能看到画面存在撕裂问题,其形成的原因是: GPU渲染之后会将结果放在 帧缓存区 中,视频控制器再通过读取帧缓存区中的数据进行 数模转换 来显示在屏幕上,显示的过程是从上至下逐行扫描进行显示,如下图画面撕裂的形成过程:假设只有一个帧缓存区的情况下,帧缓存区首先放了图1,屏幕首先对图1进行由上至下的扫描,但在扫描到图2的位置时,GPU又渲染了一张新的图片放到了帧缓存区中(图3),此时屏幕将会继续图2的位置进行扫描帧缓存区中的图片,即是此时的图3,则屏幕通过由上至下的扫描最终得到的结果就将是图4展示的样子,此时即形成了画面撕裂

画面撕裂的形成过程

1.2苹果解决画面撕裂的策略

苹果为应对画面撕裂问题采取了垂直同步+双缓存的策略。
垂直同步(Vertical synchronization):在扫面的过程中加入垂直同步信号,确保只有当前帧的图片扫面完成之后才会继续扫面下一帧的图片。
双缓存区:即采用两个缓存区来存储图片,屏幕交替扫描两个缓存区来进行显示。

苹果官方关于双缓存区示意图

虽然垂直同步+双缓存的策略解决了画面撕裂问题,但同时也引入了另一个问题:掉帧掉帧最直观的体现就是屏幕的卡顿,其形成的原因是:当接收到垂直同步信号的时候,CPUGPU还没有准备好相应的数据,即此时帧缓存区(FrameBuffer)不存在将要显示的数据,视频控制器拿不到新的数据,就会重复对上一帧的数据进行渲染

掉帧形成的示意图

为了应对掉帧问题,人们又采用的三缓存区,但掉帧归根结底的主要原因是CPUGPU处理速度问题,三缓存区虽然能在一定程度上抑制掉帧问题,但并不能从根本上解决。

1.3屏幕卡顿的原因
  • CPUGPU渲染流水线耗时过长,造成掉帧
  • 垂直同步+双缓存的策略以掉帧为代价来解决屏幕撕裂问题;
  • 三缓存区更合理的使用CPUGPU,减少掉帧的次数,但是并不能从根本上解决掉帧问题。

二、离屏渲染

2.1离屏渲染的触发

我们一般认为圆角会触发离屏渲染,但设置圆角就一定会触发离屏渲染吗?首先我们来看一个简单的demo:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //1.按钮存在背景图片
    UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
    btn1.frame = CGRectMake(100, 130, 100, 100);
    btn1.layer.cornerRadius = 50;
    [self.view addSubview:btn1];
    [btn1 setImage:[UIImage imageNamed:@"image"] forState:UIControlStateNormal];
    btn1.clipsToBounds = YES;
    
    //2.按钮不存在背景图片
    UIButton *btn2 = [UIButton buttonWithType:UIButtonTypeCustom];
    btn2.frame = CGRectMake(100, 280, 100, 100);
    btn2.layer.cornerRadius = 50;
    btn2.backgroundColor = [UIColor redColor];
    [self.view addSubview:btn2];
    btn2.clipsToBounds = YES;
    
    //3.UIImageView 设置了图片+背景
    UIImageView *imageV1 = [[UIImageView alloc] init];
    imageV1.frame = CGRectMake(100, 430, 100, 100);
    imageV1.backgroundColor = [UIColor blueColor];
    [self.view addSubview:imageV1];
    imageV1.layer.cornerRadius = 50;
    imageV1.layer.masksToBounds = YES;
    imageV1.image = [UIImage imageNamed:@"image"];
    
    //4.UIImageView 只设置了图片 无背景色
    UIImageView *imageV2 = [[UIImageView alloc] init];
    imageV2.frame = CGRectMake(100, 580, 100, 100);
    [self.view addSubview:imageV2];
    imageV2.layer.cornerRadius = 50;
    imageV2.layer.masksToBounds = YES;
    imageV2.image = [UIImage imageNamed:@"image"];
    
}

运行,并设置模拟器,Debug -> Color Off-screen rendered 标记出离屏渲染的部分:

离屏渲染提示

这样我们就会得到如下结果:
运行结果

被标记出黄色的部分是触发了离屏渲染的,而未被标记的则没有触发离屏渲染,由此可见设置了圆角不一定就会触发离屏渲染,那么触发离屏渲染的条件到底是什么呢?

2.2离屏渲染的探究

通常情况下的渲染流程是这样的:
APP渲染流程

APP通过CPUGPU的合作,不断的将渲染的内容放到帧缓冲区(Frame Buffer)中,屏幕不断的从帧缓冲区中拿到要展示的内容,实时的显示在屏幕上。

离屏渲染的流程是这样的:

离屏渲染流程

与普通的渲染不同,离屏渲染需要创建额外的离屏渲染缓冲区(offscreen Buffer),将渲染好的内容放入其中,再等到合适的时机将离屏渲染缓冲区中的内容进行叠加、合并,之后再放入帧缓冲区中。
从流程图我们可以看出,离屏渲染时,需要APP提前将部分渲染能容保存到离屏渲染缓冲区,必要的时候需要对Offscreen BufferFrame Buffer进行切换,所以势必需要更多的处理时间,而且由于离屏渲染需要开辟额外的空间,大量的离屏渲染对势必也会消耗大量的内存。与此同时,离屏渲染缓冲区也是有大小限制的,不能超过屏幕像素点的2.5倍。
大量的离屏渲染容易造成掉帧,所以很多情况下我们能避则避。但有时我们需要实现一些特殊的效果,需要Offscreen Buffer保存渲染的中间状态时,我们也不得不使用离屏渲染
以苹果提供的毛玻璃效果UIBlurEffectView为例:

UIVisualEffectView with UIBlurEffect Rendering passes

整个过程需要经历,渲染内容->捕获内容->水平模糊->垂直模糊->合并形成毛玻璃效果,根据我们对帧缓冲区的了解,为节省空间,帧缓冲区中的内容绘制到屏幕上之后就会直接移除,无法做到如此复杂的特效,该过程需要在离屏缓冲区进行处理。
有时我们也会为了提高复用效率通过layer的光栅化 shouldRasterize主动开启离屏渲染,苹果关于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.

开启光栅化后,会触发离屏渲染Render Server 会强制将 CALayer的渲染位图结果bitmap保存下来,这样下次再需要渲染时就可以直接复用,从而提高效率。
而保存的 bitmap 包含layer 的 subLayer、圆角、阴影、组透明度 group opacity 等,所以如果layer的构成包含上述几种元素,结构复杂且需要反复利用,那么就可以考虑打开光栅化
圆角、阴影、组透明度等会由系统自动触发离屏渲染,那么打开光栅化可以节约第二次及以后的渲染时间。而多层 subLayer 的情况由于不会自动触发离屏渲染,所以相比之下会多花费第一次离屏渲染的时间,但是可以节约后续的重复渲染的开销。
shouldRasterize的使用也有一定的限制:

  • 如果layer 不能被复用,则没有必要打开光栅化
  • 如果layer不是静态,需要被频繁修改,比如处于动画之中,那么开启离屏渲染反而影响效率;
  • 离屏渲染缓存内容有时间限制,缓存内容 100ms 内如果没有被使用,那么就会被丢弃,无法进行复用;
  • 离屏渲染缓存空间有限,超过 2.5 倍屏幕像素大小的话也会失效,无法复用。
    layer的构成

由上图我们可以看出layer由三部分组成,通常我们设置圆角会设置layercornerRadius,关于cornerRadiusapple的解释如下:

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.

由上述可知,如果我们只是设置了cornerRadius属性,并不会对content进行裁剪,只有我们设置masksToBounds才会对内容进行裁剪。
图层的叠加大致遵循“画家算法”,即由远及近绘制图层显示在屏幕上:

画家算法

我们可以试想下:如果我们要对一个拥有多个图层构成的视图进行圆角设置,如果是存在帧缓冲区,那么就会存在一个问题,每渲染一帧就会丢前面的一帧数据,当我们要设置圆角时,前面的图层早已丢失,而离屏缓存区不同,离屏缓存区会对渲染的图层保留一段时间,这段时间就足以我们对多图层进行、合并、设置圆角等操作。想要触发离屏渲染不单单是说设置了masksToBounds就会触发,我们更多的要在意的是我们所要操作的图层,是否需要保留中间图层,如果只是单图层,肯定不会触发离屏渲染
值得注意的是,重写 drawRect: 方法并不会触发离屏渲染。重写 drawRect:会将 GPU中的渲染操作转移到 CPU中完成,并且需要额外开辟内存空间。

2.3圆角处理的参考方案
  • 方案一:
    最简单的方法就是找UI切带圆角的图片。
  • 方案二:
- (UIImage *)roundedCornerImageWithCornerRadius:(CGFloat)cornerRadius {
    CGFloat w = self.size.width;
    CGFloat h = self.size.height;
    CGFloat scale = [UIScreen mainScreen].scale;
    //防止圆角半径小于0,或者大于宽/高中较小值的一半。
    if (cornerRadius < 0) {
        cornerRadius = 0;
    }else if (cornerRadius > MIN(w, h)/2.0){
        cornerRadius = MIN(w, h)/2.0;
    }
    
    UIImage *image = nil;
    CGRect imageFrame = CGRectMake(0, 0, w, h);
    UIGraphicsBeginImageContextWithOptions(self.size, NO, scale);
    [[UIBezierPath bezierPathWithRoundedRect:imageFrame cornerRadius:cornerRadius] addClip];
    [self drawInRect:imageFrame];
    image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return image;
}
  • 方案三
+ (UIImage *)addMaskToBounds:(CGRect)maskBounds image:(UIImage *)image cornerRadius:(CGFloat)cornerRadius {
    CGFloat w = maskBounds.size.width;
    CGFloat h = maskBounds.size.height;
    CGSize size = maskBounds.size;
    CGFloat scale = [UIScreen mainScreen].scale;
    CGRect imageRect = CGRectMake(0, 0, w, h);
    if (cornerRadius < 0) {
        cornerRadius = 0;
    }else if (cornerRadius > MIN(w, h)/2.0){
        cornerRadius = MIN(w, h)/2.0;
    }
    UIGraphicsBeginImageContextWithOptions(size, NO, scale);
    [[UIBezierPath bezierPathWithRoundedRect:imageRect cornerRadius:cornerRadius] addClip];
    [image drawInRect:imageRect];
    image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return image;
}

  • 方案四
@interface RoundImageView()
@property (nonatomic, strong) UIImageView *maskImageView;
@end

@implementation RoundImageView

- (instancetype)init {
    self = [super init];
    if (self) {
        _maskImageView = [[UIImageView alloc] initWithFrame:CGRectZero];
        _maskImageView.image = [UIImage imageNamed:@"ic_imageView_mask"];//加圆角图片盖在上面
        [self addSubview:_maskImageView];
    }
    return self;
}

- (void)layoutSubviews {
    [super layoutSubviews];
    CGRect bounds = self.bounds;
    _maskImageView.frame = bounds;
}

另附:YYImage的圆角处理


YYImage的圆角处理
2.4常见触发离屏渲染的几种情况
  • 使用了masklayerlayer.mask
  • 需要进行裁剪的layer(layer.masksToBounds / view.clipsToBounds)
  • 设置了组透明度为Yes,并且透明度不为1的layer(layer.allowsGroupOpacity / layer.opacity)
  • 添加了投影的layer (layer.shadow)
  • 采用了光栅化layerlayer.shouldRasterize
  • 绘制了文字的layerUILabel, CATextLayer, CoreText等)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,270评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,489评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,630评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,906评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,928评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,718评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,442评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,345评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,802评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,984评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,117评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,810评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,462评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,011评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,139评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,377评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,060评论 2 355