离屏渲染之我知

渲染

相比于当前屏幕渲染,离屏渲染的代价是很高的,这也是iOS移动端优化的必要部分。

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

1.On-Screen Rendering
意为当前屏幕渲染,指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行。

2.Off-Screen Rendering
意为离屏渲染,指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。

OpenGL ES是一套多功能开放标准的用于嵌入系统的C-based的图形库,用于2D和3D数据的可视化。OpenGL被设计用来转换一组图形调用功能到底层图形硬件(GPU),由GPU执行图形命令,用来实现复杂的图形操作和运算,从而能够高性能、高帧率利用GPU提供的2D和3D绘制能力。图层属性的混合体没有预合成之前不能直接在屏幕中绘制,所以就需要屏幕外渲染。屏幕外渲染并不意味着软件绘制,但是它意味着图层必须在被显示之前在一个屏幕外上下文中被渲染(不论CPU还是GPU)。

开辟缓存区渲染

一般情况下,OpenGL会将应用提交到Render Server的动画直接渲染显示(基本的Tile-Based渲染流程),但对于一些复杂的图像动画的渲染并不能直接渲染叠加显示,而是需要根据Command Buffer分通道进行渲染之后再组合,这一组合过程中,就有些渲染通道是不会直接显示的;Masking渲染需要更多渲染通道和合并的步骤;而这些没有直接显示在屏幕的上的通道就是Offscreen Rendering Pass。

Offscreen Render为什么卡顿,Offscreen Render需要更多的渲染通道,而且不同的渲染通道间切换需要耗费一定的时间,这个时间内GPU会闲置,当通道达到一定数量,对性能也会有较大的影响。

离屏渲染的触发

离屏渲染可以被 Core Animation 自动触发,或者被应用程序强制触发。屏幕外的渲染会合并渲染图层树的一部分到一个新的缓冲区,然后该缓冲区被渲染到屏幕上。

离屏渲染合成计算是非常昂贵的, 但有时你也许希望强制这种操作。一种好的方法就是缓存合成的纹理/图层。如果你的渲染树非常复杂(所有的纹理,以及如何组合在一起),你可以强制离屏渲染缓存那些图层,然后可以用缓存作为合成的结果放到屏幕上。

如果你的程序混合了很多图层,并且想要他们一起做动画,GPU 通常会为每一帧(1/60s)重复合成所有的图层。当使用离屏渲染时,GPU 第一次会混合所有图层到一个基于新的纹理的位图缓存上,然后使用这个纹理来绘制到屏幕上。现在,当这些图层一起移动的时候,GPU 便可以复用这个位图缓存,并且只需要做很少的工作。需要注意的是,只有当那些图层不改变时,这才可以用。如果那些图层改变了,GPU 需要重新创建位图缓存。你可以通过设置 shouldRasterize 为 YES 来触发这个行为。

然而,这是一个权衡。第一,这可能会使事情变得更慢。创建额外的屏幕外缓冲区是 GPU 需要多做的一步操作,特殊情况下这个位图可能再也不需要被复用,这便是一个无用功了。然而,可以被复用的位图,GPU 也有可能将它卸载了。所以你需要计算 GPU 的利用率和帧的速率来判断这个位图是否有用。

离屏渲染也可能产生副作用。如果你正在直接或者间接的将mask应用到一个图层上,Core Animation 为了应用这个 mask,会强制进行屏幕外渲染。这会对 GPU 产生重负。通常情况下 mask 只能被直接渲染到帧的缓冲区中(在屏幕内)。

Instrument 的 Core Animation 工具有一个叫做Color Offscreen-Rendered Yellow的选项,它会将已经被渲染到屏幕外缓冲区的区域标注为黄色(这个选项在模拟器中也可以用)。同时记得检查Color Hits Green and Misses Red选项。绿色代表无论何时一个屏幕外缓冲区被复用,而红色代表当缓冲区被重新创建。

一般情况下,你需要避免离屏渲染,因为这是很大的消耗。直接将图层合成到帧的缓冲区中(在屏幕上)比先创建屏幕外缓冲区,然后渲染到纹理中,最后将结果渲染到帧的缓冲区中要廉价很多。因为这其中涉及两次昂贵的环境转换(转换环境到屏幕外缓冲区,然后转换环境到帧缓冲区)。

所以当你打开Color Offscreen-Rendered Yellow后看到黄色,这便是一个警告,但这不一定是不好的。如果 Core Animation 能够复用屏幕外渲染的结果,这便能够提升性能。

同时还要注意,rasterized layer 的空间是有限的。苹果暗示大概有屏幕大小两倍的空间来存储 rasterized layer/屏幕外缓冲区。

如果你最后设置了 shouldRasterize 为 YES,那也要记住设置 rasterizationScale 为 contentsScale。

离屏渲染的体现

相比于当前屏幕渲染,离屏渲染的代价是很高的,主要体现在两个方面:

1.创建新缓冲区
要想进行离屏渲染,首先要创建一个新的缓冲区。

2.上下文切换
离屏渲染的整个过程,需要多次切换上下文环境:先是从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上有需要将上下文环境从离屏切换到当前屏幕。而上下文环境的切换是要付出很大代价的。

iOS 的显示系统是由 VSync 信号驱动的,VSync 信号由硬件时钟生成,每秒钟发出 60 次(这个值取决设备硬件,比如 iPhone 真机上通常是 59.97)。iOS 图形服务接收到 VSync 信号后,会通过 IPC 通知到 App 内。App 的 Runloop 在启动后会注册对应的 CFRunLoopSource 通过 mach_port 接收传过来的时钟信号通知,随后 Source 的回调会驱动整个 App 的动画与显示。

在 VSync 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 App,App 主线程开始在 CPU 中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等。随后 CPU 会将计算好的内容提交到 GPU 去,由 GPU 进行变换、合成、渲染。随后 GPU 会把渲染结果提交到帧缓冲区去,等待下一次 VSync 信号到来时显示到屏幕上。由于垂直同步的机制,如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。从上图中可以看到,CPU 和 GPU 不论哪个阻碍了显示流程,都会造成掉帧现象。所以开发时,也需要分别对 CPU 和 GPU 压力进行评估和优化。

Core Animation 在 RunLoop 中注册了一个 Observer,监听了 BeforeWaiting 和 Exit 事件。当一个触摸事件到来时,RunLoop 被唤醒,App 中的代码会执行一些操作,比如创建和调整视图层级、设置 UIView 的 frame、修改 CALayer 的透明度、为视图添加一个动画;这些操作最终都会被 CALayer 标记,并通过 CATransaction 提交到一个中间状态去。当上面所有操作结束后,RunLoop 即将进入休眠(或者退出)时,关注该事件的 Observer 都会得到通知。这时 Core Animation 注册的那个 Observer 就会在回调中,把所有的中间状态合并提交到 GPU 去显示;如果此处有动画,通过 DisplayLink 稳定的刷新机制会不断的唤醒runloop,使得不断的有机会触发observer回调,从而根据时间来不断更新这个动画的属性值并绘制出来。

为了不阻塞主线程,Core Animation 的核心是 OpenGL ES 的一个抽象物,所以大部分的渲染是直接提交给GPU来处理。 而Core Graphics/Quartz 2D的大部分绘制操作都是在主线程和CPU上同步完成的,比如自定义UIView的drawRect里用CGContext来画图。

那哪些情况会Offscreen Render呢?

1) drawRect
2) layer.shouldRasterize = true;
3) 有mask或者是阴影(layer.masksToBounds, layer.shadow*);
 3.1) shouldRasterize(光栅化)
 3.2) masks(遮罩)
 3.3) shadows(阴影)
 3.4) edge antialiasing(抗锯齿)
 3.5) group opacity(不透明)
4) Text(UILabel, CATextLayer, Core Text, etc)...

注:layer.cornerRadius,layer.borderWidth,layer.borderColor并不会Offscreen Render,因为这些不需要加入Mask。 iOS 9.0 之前UIimageView跟UIButton设置圆角都会触发离屏渲染。iOS 9.0 之后UIButton设置圆角会触发离屏渲染,而UIImageView里png图片设置圆角不会触发离屏渲染了,如果设置其他阴影效果之类的还是会触发离屏渲染的。

需要注意的是,如果shouldRasterize被设置成YES,在触发离屏绘制的同时,会将光栅化后的内容缓存起来,如果对应的layer及其sublayers没有发生改变,在下一帧的时候可以直接复用。这将在很大程度上提升渲染性能。而其它属性如果是开启的,就不会有缓存,离屏绘制会在每一帧都发生。

圆角优化,可以使用贝塞尔曲线UIBezierPath和Core Graphics框架画出一个圆角,也可以使用CAShapeLayer和UIBezierPath设置圆角。CAShapeLayer继承于CALayer,可以使用CALayer的所有属性值,CAShapeLayer需要贝塞尔曲线配合使用才有意义(也就是说才有效果)。使用CAShapeLayer(属于CoreAnimation)与贝塞尔曲线可以实现不在view的drawRect(继承于CoreGraphics走的是CPU,消耗的性能较大)方法中画出一些想要的图形,CAShapeLayer动画渲染直接提交到手机的GPU当中,相较于view的drawRect方法使用CPU渲染而言,其效率极高,能大大优化内存使用情况。总的来说就是用CAShapeLayer的内存消耗少,渲染速度快。

Core Graphics

UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(100,100,100,100)];

imageView.image = [UIImage imageNamed:@"xx"];

//开始对imageView进行画图

UIGraphicsBeginImageContextWithOptions(imageView.bounds.size,NO,1.0);

//使用贝塞尔曲线画出一个圆形图

[[UIBezierPath bezierPathWithRoundedRect:imageView.bounds cornerRadius:imageView.frame.size.width] addClip];

[imageView drawRect:imageView.bounds];

imageView.image = UIGraphicsGetImageFromCurrentImageContext();

//结束画图

UIGraphicsEndImageContext();

[self.view addSubview:imageView];

CAShapeLayer 方式

UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)]; 
imageView.image = [UIImage imageNamed:@"myImg"]; 
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:imageView.bounds byRoundingCorners:UIRectCornerAllCorners cornerRadii:imageView.bounds.size];
CAShapeLayer *maskLayer = [[CAShapeLayer alloc]init]; 
//设置大小 
maskLayer.frame = imageView.bounds; 
//设置图形样子 
maskLayer.path = maskPath.CGPath;
imageView.layer.mask = maskLayer; 
[self.view addSubview:imageView];

至于 mask,圆角半径(特殊的mask)和 clipsToBounds/masksToBounds,你可以简单的为一个已经拥有 mask 的 layer 创建内容,比如,已经应用了 mask 的 layer 使用一张图片。如果你想根据 layer 的内容为其应用一个长方形 mask,你可以使用 contentsRect 来代替蒙板。

对于shadow,如果图层是个简单的几何图形或者圆角图形,我们可以通过设置shadowPath来优化性能,能大幅提高性能。

imageView.layer.shadowColor = [UIColor redColor].CGColor;

imageView.layer.shadowOpacity = 1.0;

imageView.layer.shadowRadius = 2.0;

UIBezierPath *path = [UIBezierPath bezierPathWithRect:imageView.frame];

imageView.layer.shadowPath = path.CGPath;

总结来说:
当我们需要圆角效果时,可以使用一张中间透明图片蒙上去
使用ShadowPath指定layer阴影效果路径
使用异步进行layer渲染(Facebook开源的异步绘制框架AsyncDisplayKit)
设置layer的opaque值为YES,减少复杂图层合成
尽量使用不包含透明(alpha)通道的图片资源
尽量设置layer的大小值为整形值
直接让美工把图片切成圆角进行显示,这是效率最高的一种方案
很多情况下用户上传图片进行显示,可以让服务端处理圆角
使用代码手动生成圆角Image设置到要显示的View上,利用UIBezierPath(CoreGraphics框架)画出来圆角图片

另一种特殊的“离屏渲染”方式:CPU渲染

如果我们重写了drawRect方法,并且使用任何Core Graphics的技术进行了绘制操作,就涉及到了CPU渲染。整个渲染过程由CPU在App内同步地完成,渲染得到的bitmap最后再交由GPU用于显示。

Core Graphics 的绘制 API 的确会触发离屏渲染,但不是那种 GPU 的离屏渲染。使用 Core Graphics 绘制 API 是在 CPU 上执行,触发的是 CPU 版本的离屏渲染。

当前屏幕渲染、离屏渲染、CPU渲染的选择

1.尽量使用当前屏幕渲染
鉴于离屏渲染、CPU渲染可能带来的性能问题,一般情况下,我们要尽量使用当前屏幕渲染。

2.离屏渲染 VS CPU渲染
由于GPU的浮点运算能力比CPU强,CPU渲染的效率可能不如离屏渲染;但如果仅仅是实现一个简单的效果,直接使用CPU渲染的效率又可能比离屏渲染好,毕竟离屏渲染要涉及到缓冲区创建和上下文切换等耗时操作。

总结

阻塞主线程的绘制任务主要是这三大类:Layout计算视图布局文本宽高、Rendering文本渲染图片解码图片绘制、UIKit对象创建更新释放。除了UIKit和CoreAnimation相关操作必须在主线程中进行,其他的都可以挪到后台线程异步执行。

一方面来说,一部分(通过drawRect并且使用任何CoreGraphics来实现的绘制,或使用CoreText[其实就是使用CoreGraphics]绘制)的确涉及到了“离屏绘制”,但是这和我们通常说的那种离屏绘制是有区别的。当你实现drawRect方法或者通过CoeGraphics绘制的时候,其实你是在使用CPU绘制。并且这个绘制的过程会在你的App内同步地进行。基本上你只是调用了一些向位图缓存内写入一些二进制信息的方法而已。

而另外一种形式离屏绘制是发生在绘制服务(是独立的处理过程)并且同时通过GPU执行(这里就不是像前面提到的用CPU了)。当OpengGL的绘制程序在绘制每个layer的时候,有可能因为包含多子层级关系而必须停下来把他们合成到一个单独的缓存里。你可能认为GPU应该总是比CPU牛逼一点,但是在这里我们还是需要慎重的考虑一下。因为对GPU来说,从当前屏幕(on-screen)到离屏(off-screen)上下文环境的来回切换(这个过程必须flush管线和光栅),代价是非常大的。因此对一些简单的绘制过程来说,这个过程有可能用CoreGraphics,全部用CPU来完成反而会比GPU做得更好。所以如果你正在尝试处理一些复杂的层级,并且在犹豫到底用-[CALayer setShouldRasterize:]还是通过CoreGraphics来绘制层级上的所有内容,唯一的方法就是测试并且进行权衡。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容