iOS 下离屏渲染的问题

Offscreen Rendering

如何检测你的项目中是否 触发了离屏渲染问题

图片
图片

那么为何有一些会触发离屏渲染,而有一些却不会触发呢?下面我们开始深入的探索。

离屏渲染的具体过程

我们知道通常的渲染流程是这样的:

图片

App通过CPU和GPU的合作,不停的将内容渲染完成放入FrameBuffer帧缓存区,而屏幕显示不断从FrameBuffer中获取内容,显示实时的内容。

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

图片

在普通的情况下,GPU直接将渲染好的内容放入FrameBuffer中,但是在离屏渲染时不同,需要先额外创建离屏渲染缓存区OffscreenBuffer。将提前渲染好的内容放入其中,等到合适的时机再将OffSreeBuffer中的内容进一步叠加、渲染。完成后将结果切换到FrameBuffer中。

离屏渲染的效率问题

从上面的流程来看,离屏渲染时,由于App需要提前对部分内容进行额外的渲染并保存到OffScreenBuffer,以及需要在必要时对OffScreenBuffer和FrameBuffer进行内容切换,所以会需要更长的处理时间。(实际上这两步切换的代价是非常大的)。

OffScreenBuffer本身就需要额外的空间,大量的离屏渲染可能造成内存过大的压力。与此同时,OffScreenBuffer的总大小也是有限的:不能超过屏幕总像素的2.5倍。

可见离屏渲染的开销非常大,一旦需要离屏渲染的内容过多,就容易造成掉帧问题,所以大部分情况下,我们要避免出现离屏渲染。

为什么要用离屏渲染

既然离屏渲染会造成性能问题,那么为什么还要使用离屏渲染?

其实主要是以下两种原因:

  1. 一些特殊的效果需要使用额外的OffScreenBuffer来保存渲染中间的状态,所以不得不使用离屏渲染
  2. 出于效率的目的,可以将内容提前渲染并保存到OffScreenBuffer中,从而达到复用的目的

例如,第一种原因,也就是不得不使用离屏渲染的情况。一般都是系统自动触发。如:阴影、圆角等。比如我们使用的蒙版(mask)功能,因为最终的结果是有超过一层的渲染结果进行叠加,所以必须要利用额外的内存空间对中间的渲染结果进行保存,因此系统会默认触发离屏渲染.

比如,iOS8开始提供模糊特效UIBlurEffectView:

  1. 先渲染需要模糊的内容本身;
  2. 对内容进行缩放;
  3. 对上一步结果进行垂直模糊;
  4. 对上一步结果进行横向模糊;
  5. 最后一步,将模糊后的结果进行叠加合成,实现最终完整的模糊效果。

在这样的5次过程,系统也会自动触发离屏渲染,用来保存复杂的特效下,利用额外的内存空间对中间的结果进行保存,以便最后进行效果的合成。

离屏渲染的第二种原因:shouldRasterize 光栅化

图片

开启光栅化后,就会主动触发离屏渲染。Render Server会强制将CALayer渲染位图结果bitmap保存下来,这样下次渲染可以直接复用,提高效率;而保存下来的bitmap就已经包含了layer和sublayer、圆角、阴影、透明度等。

如果layer的构成包含了以上几种元素,结构非常复杂且还需要重复利用,可以考虑开启光栅化;因为layer 上的圆角、阴影、透明度等会由系统自动触发离屏渲染,那么打开光栅化就可以节约第二次以及以后的渲染时间。

而多层的subLayer的情况由于不会自动触发离屏渲染,所以相比之下会花费第一次离屏渲染的时间,但是可以节约后续重复的渲染开销。

使用光栅化的注意点:

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

圆角的离屏渲染探索

通常来讲,设置了Layer的圆角效果后,会自动触发离屏渲染,但是具体什么情况下设置圆角才会触发离屏渲染?

图片

如上图所示,Layer由3层组成,我们设置圆角通常是用下面的代码:

<pre data-tool="mdnice编辑器" style="margin: 10px 0px; padding: 0px; outline: 0px; max-width: 100%; border-top-left-radius: 5px; border-top-right-radius: 5px; border-bottom-right-radius: 5px; border-bottom-left-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px; box-sizing: border-box !important; word-wrap: break-word !important;">view.layer.cornerRadius = 2; </pre>

CornerRadius - Apple 官方介绍

图片

根据苹果的描述,上面这句代码,只会默认设置backgroundColor和border的圆角,而不会设置content的圆角,除非设置了layer.maskToBounds为true(对应view的clickToBounds属性)。

如果只设置了CornerRadius而没有设置masksToBounds,由于不需要叠加裁剪,此时是不会触发离屏渲染的。而当设置了裁剪属性时,由于maskToBounds会对layer以及所有 的subLayer的content都进行裁剪,所以不得不触发离屏渲染。

<pre data-tool="mdnice编辑器" style="margin: 10px 0px; padding: 0px; outline: 0px; max-width: 100%; border-top-left-radius: 5px; border-top-right-radius: 5px; border-bottom-right-radius: 5px; border-bottom-left-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px; box-sizing: border-box !important; word-wrap: break-word !important;">view.layer.masksToBounds = true //触发离屏渲染的原因 </pre>

离屏渲染的逻辑

刚才说圆角加上masksToBounds时,因为masksToBounds会对layer上的所有内容进行裁剪,从而诱发了离屏渲染,那么这个过程具体是怎么回事呢?我们来仔细研究一下:

  • 在普通的layer绘制中,上层的subLayer会覆盖下层的subLayer,下层的subLayer在绘制完成后就可以抛弃了,从而节约空间提高效率。
  • 所有subLayer一次绘制完毕后,整个绘制过程完成,就可以进行后续的呈现了。假设我们需要绘制一个三层的subLayer,并不设置裁剪和圆角,那么整个绘制过程就如下图所示:
图片

绘制完进行display

设置了CornerRadius以及masksToBounds

当我们设置了CornerRadius以及masksToBounds进行圆角加裁剪时,masksToBounds裁剪属性会应用到所有的subLayer上,也就意味着所有的subLayer都要进行圆角+裁剪,意味着所有的subLayer在第一次绘制后,并不能立刻丢弃,而必须保存在OffScreenBuffer中等待下一轮的圆角加裁剪操作,这样便引发了离屏渲染。

实际上,并不单只有圆角加裁剪会触发离屏渲染。如果设置了透明度和组透明(layer.allowsGroupOpacity+layer.opacity),阴影属性(shadowOffset)等,都会产生这样的离屏渲染,因为这些都不是对单一的layer进行处理,而是对layer及其所有 的subLayer进行处理,从而引发离屏渲染。

避免圆角离屏渲染的手段:

除了尽量减少圆角裁剪的使用,还有什么别的办法可以避免圆角+裁剪引起的离屏渲染?

由于刚才提到,圆角引起离屏渲染的本质是裁剪的叠加,导致了masksToBounds对layer及其所有的subLayer进行了二次处理,那么我们只要避免使用masksToBounds进行二次处理,而是对所有的subLayer进行预处理,就可以只进行“画家算法(先绘制离屏幕较远的图层,然后绘制距离屏幕较近的图层,根据深度值,确定绘制顺序)”,用一次叠加就完成绘制。

有哪些可行方案

1.换资源 直接使用带圆角的图片,或者替换背景色为带圆角的纯色背景图,从而避免使用圆角裁剪。不过这周方法需要依赖具体情况,并不通用 。

2.mask 再增加一个和背景色相同的遮罩mask覆盖在最上层,盖住四个角,营造出圆角的形状。但这种方式难以解决背景色为图片 或渐变色的情况。注意这里的mask并不是指的layer上的mask,而是用两个view的叠加

3.UIBezierPath 用贝塞尔曲线绘制闭合带圆角的矩形,在上下文设置只有内部可见,再将不带圆角的layer渲染成图片,添加到贝塞尔矩形中。这种方法效率较高,但是layer的布局一旦改变,贝塞尔曲线都需要手动进行重新绘制,所以需要对frame、color等进行手动监听并重绘。

4.CoreGraphics 重写 drawRect:,用coreGraphics相关方法,在需要应用圆角时进行手动绘制。不过CoreGraphics效率也有限,如果多次调用也会有效率问题。

触发离屏渲染的几种情况

  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,CoreText等)
举几个例子,加深下理解:
//按钮存在背景图片  因为 会对button的layer和其imageView的layer进行圆角加裁剪 所以会触发离屏渲染
UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
btn1.frame = CGRectMake(100, 30, 100, 100);
[btn1 setImage:[UIImage imageNamed:@"btn.png"] forState:UIControlStateNormal];
[self.view addSubview:btn1];
btn1.layer.cornerRadius = 50;
btn1.clipsToBounds = YES;
//按钮存在背景图片  只设置了clipsToBounds,裁剪是针对于imageview的,所以不会触发离屏渲染(not offscreen rendering)
UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
btn1.frame = CGRectMake(100, 30, 100, 100);
[btn1 setImage:[UIImage imageNamed:@"btn.png"] forState:UIControlStateNormal];
[self.view addSubview:btn1];
btn1.imageView.layer.cornerRadius = 50;
btn1.clipsToBounds = YES;
//按钮不存在背景图片 虽然设置了圆角+裁剪,但是只有一层layer  所以不会触发离屏渲染
UIButton *btn2 = [UIButton buttonWithType:UIButtonTypeCustom];
btn2.frame = CGRectMake(100, 180, 100, 100);
btn2.backgroundColor = [UIColor blueColor];
[self.view addSubview:btn2];
btn2.layer.cornerRadius = 50;
btn2.clipsToBounds = YES;
//UIImageView 设置了图片+背景色; 背景色和图片两层layer都需要进行圆角+裁剪  所以会触发离屏渲染
UIImageView *img1 = [[UIImageView alloc]init];
img1.frame = CGRectMake(100, 320, 100, 100);
img1.backgroundColor = [UIColor blueColor];
img1.image = [UIImage imageNamed:@"btn.png"];
[self.view addSubview:img1];
img1.layer.cornerRadius = 50;
img1.layer.masksToBounds = YES;
//UIImageView 只设置了图片,无背景色; 只有一层 layer 需要圆角+裁剪 所以不会触发离屏渲染
UIImageView *img2 = [[UIImageView alloc]init];
img2.frame = CGRectMake(100, 480, 100, 100);
img2.image = [UIImage imageNamed:@"btn.png"];
[self.view addSubview:img2];
img2.layer.cornerRadius = 50;
img2.layer.masksToBounds = YES;
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容