离屏渲染(Offscreen rendering)
离屏渲染的定义
离屏渲染(offscreen-rendering)
顾名思义为屏幕外的渲染,即渲染的结果不会直接呈现到当前屏幕上,而是等待合适的时机才会被显示。
图上,正常情况下,在当前屏幕显示的内容,由 GPU 渲染完成后放到当前屏幕的帧缓存区,不需要额外的渲染空间。我们知道iPhone 的屏幕刷新率是 60Hz
,也就是刷新一帧的时间是 16.67 ms,每隔这段时间视频控制器就会去读一次缓存区的内容来显示。
假如GPU
遇到性能瓶颈,导致无法在一帧内更新渲染结果到帧缓存区,那么从缓存区读到的会是上一帧的内容,导致帧率降低界面卡顿。
苹果对于页面流畅度的要求是非常苛刻的,如果页面布局比较复杂,硬件遇到瓶颈,不能任由它卡顿。离屏渲染的机制就被引入,它会触发比较消耗性能的视图提前渲染。
如果要在显示屏上显示内容,我们至少需要一块与屏幕像素数据量一样大的
frame buffer
,作为像素数据存储区域,而这也是GPU
存储渲染结果的地方。如果有时因为面临一些限制,无法把渲染结果直接写入frame buffer
,而是先暂存在另外的内存区域,之后再写入frame buffer
,那么这个过程被称之为离屏渲染。
1. CPU”离屏渲染“-不是真正意义的离屏渲染
大家知道,如果我们在
UIView
中实现了drawRect
方法,就算它的函数体内部实际没有代码,系统也会为这个view
申请一块内存区域,等待CoreGraphics
可能的绘画操作。
对于类似这种“新开一块CGContext
来画图“的操作,有很多文章和视频也称之为“离屏渲染
”(因为像素数据
是暂时存入了CGContext
,而不是直接到了frame buffer
)。进一步来说,其实所有CPU进行的光栅化
操作(如文字渲染、图片解码),都无法直接绘制到由 GPU 掌管的 frame buffer ,只能暂时先放在另一块内存之中,说起来都属于“离屏渲染”。
自然我们会认为,因为CPU不擅长做这件事,所以我们需要尽量避免它,就误以为这就是需要避免离屏渲染的原因。但是根据`苹果工程师的说法``
👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇
Cross-posted from the comments section there:
[The] advice as far as the button is good here, but I’ve got one small correction and some bonus explanation for interested readers.
I’d like to clarify a few points about offscreen drawing as described in this post. While your list of cases which might elicit offscreen drawing is accurate, there are two grossly different mechanisms being triggered by elements of this list (each with different performance characteristics), and it’s possible that a single view will require both. Those two mechanisms have very different performance considerations.
In particular, a few (implementing drawRect and doing any CoreGraphics drawing, drawing with CoreText [which is just using CoreGraphics]) are indeed “offscreen drawing,” but they’re not what we usually mean when we say that. They’re very different from the rest of the list. When you implement drawRect or draw with CoreGraphics, you’re using the CPU to draw, and that drawing will happen synchronously within your application. You’re just calling some function which writes bits in a bitmap buffer, basically.
The other forms of offscreen drawing happen on your behalf in the render server (a separate process) and are performed via the GPU (not via the CPU, as suggested in the previous paragraph). When the OpenGL renderer goes to draw each layer, it may have to stop for some subhierarchies and composite them into a single buffer. You’d think the GPU would always be faster than the CPU at this sort of thing, but there are some tricky considerations here. It’s expensive for the GPU to switch contexts from on-screen to off-screen drawing (it must flush its pipelines and barrier), so for simple drawing operations, the setup cost may be greater than the total cost of doing the drawing in CPU via e.g. CoreGraphics would have been. So if you’re trying to deal with a complex hierarchy and are deciding whether it’s better to use –[CALayer setShouldRasterize:] or to draw a hierarchy’s contents via CG, the only way to know is to test and measure.
You could certainly end up doing two off-screen passes if you draw via CG within your app and display that image in a layer which requires offscreen rendering. For instance, if you take a screenshot via –[CALayer renderInContext:] and then put that screenshot in a layer with a shadow.
Also: the considerations for shouldRasterize are very different from masking, shadows, edge antialiasing, and group opacity. If any of the latter are triggered, there’s no caching, and offscreen drawing will happen on every frame; rasterization does indeed require an offscreen drawing pass, but so long as the rasterized layer’s sublayers aren’t changing, that rasterization will be cached and repeated on each frame. And of course, if you’re using drawRect: or drawing yourself via CG, you’re probably caching locally. More on this in “Polishing Your Rotation Animations,” WWDC 2012.
Speaking of caching: if you’re doing a lot of this kind of drawing all over your application, you may need to implement cache-purging behavior for all these (probably large) images you’re going to have sitting around on your application’s heap. If you get a low memory warning, and some of these images are not actively being used, it may be best for you to get rid of those stretchable images you drew (and lazily regenerate them when needed). But that may end up just making things worse, so testing is required there too.
CPU渲染并非真正意义上的离屏渲染。另一个证据是,如果你的view实现了drawRect,此时打开Xcode调试的“Color offscreen rendered yellow”开关,你会发现这片区域不会被标记为黄色,说明Xcode并不认为这属于离屏渲染。
其实通过CPU渲染就是俗称的“软件渲染”,而真正的离屏渲染发生在GPU。
2. GPU离屏渲染
在上面的渲染流水线示意图中我们可以看到,主要的渲染操作都是由CoreAnimation
的Render Server模块,通过调用显卡驱动所提供的
OpenGL/Metal接口来执行的。通常对于每一层
layer,
Render Server`会遵循“画家算法”,按次序输出到frame buffer,后一层覆盖前一层,就能得到最终的显示结果(值得一提的是,与一般桌面架构不同,在iOS中,设备主存和GPU的显存共享物理内存,这样可以省去一些数据传输开销)。
然而有些场景并没有那么简单。作为“画家”的GPU虽然可以一层一层往画布上进行输出,但是无法在某一层渲染完成之后,再回过头来擦除/改变其中的某个部分——因为在这一层之前的若干层layer像素数据,已经在渲染中被永久覆盖了。这就意味着,对于每一层layer,要么能找到一种通过单次遍历就能完成渲染的算法,要么就不得不另开一块内存,借助这个临时中转区域来完成一些更复杂的、多次的修改/剪裁操作。
如果要绘制一个带有圆角并剪切圆角以外内容的容器,就会触发离屏渲染。我的猜想是(如果读者中有图形学专家希望能指正):
将一个layer
的内容裁剪成圆角,可能不存在一次遍历就能完成的方法
容器的子layer
因为父容器有圆角,那么也会需要被裁剪,而这时它们还在渲染队列中排队,尚未被组合到一块画布上,自然也无法统一裁剪
此时我们就不得不开辟一块独立于
frame buffer
的空白内存,先把容器以及其所有子layer依次画好,然后把四个角“剪”成圆形,再把结果画到frame buffer
中。这就是GPU
的离屏渲染。
附加 : 离屏渲染 VS CPU 渲染
由于 GPU 的浮点运算能力比 CPU 强,CPU 渲染的效率可能不如离屏渲染;但如果仅仅是实现一个简单的效 果,直接使用 CPU 渲染的效率又可能比离屏渲染好,毕竟离屏渲染要涉及到缓冲区创建和上下文切换等耗 时操作
UIButton
的masksToBounds = YES
又设置 setImage
、setBackgroundImage
、[button setBackgroundColor:[UIColor colorWithPatternImage:[UIImage imageNamed:@"btn_selected"]]]
;
下发生离屏渲染,但是[button setBackgroundColor:[UIColor redColor]]
;是不会出现离屏渲染的
关于UIImageView
,现在测试发现(现版本: iOS10),在性能的范围之内,给 UIImageView
设置圆角是不会触发离
屏渲染的,但是同时给 UIImageView
设置背景色则肯定会触发.触发离屏渲染跟 png.jpg 格式并无关联 日常我们使用 layer 的两个属性,实现圆角
imageView.layer.cornerRaidus = CGFloat(10);
imageView.layer.masksToBounds = YES;
这样处理的渲染机制是 GPU 在当前屏幕缓冲区外新开辟一个渲染缓冲区进行工作,也就是离屏渲染,这会 给我们带来额外的性能损耗。如果这样的圆角操作达到一定数量,会触发缓冲区的频繁合并和上下文的的 频繁切换,性能的代价会宏观地表现在用户体验上——掉帧
3. 触发场景
3.1 cornerRadius+clipsToBounds
原因就如同上面提到的,不得已只能另开一块内存来操作。而如果只是设置cornerRadius(如不需要剪切内容,只需要一个带圆角的边框),或者只是需要裁掉矩形区域以外的内容(虽然也是剪切,但是稍微想一下就可以发现,对于纯矩形而言,实现这个算法似乎并不需要另开内存),并不会触发离屏渲染。关于剪切圆角的性能优化,根据场景不同有几个方案可供选择,非常推荐阅读AsyncDisplayKit中的一篇文档。
3.2 shadow
其原因在于,虽然layer
本身是一块矩形区域,但是阴影默认是作用在其中”非透明区域“的,而且需要显示在所有layer内容的下方,因此根据画家算法必须被渲染在先。但矛盾在于此时阴影的本体(layer
和其子layer
)都还没有被组合到一起,怎么可能在第一步就画出只有完成最后一步之后才能知道的形状呢?这样一来又只能另外申请一块内存,把本体内容都先画好,再根据渲染结果的形状,添加阴影到frame buffer
,最后把内容画上去(这只是我的猜测,实际情况可能更复杂)。不过如果我们能够预先告诉CoreAnimation
(通过shadowPath
属性)阴影的几何形状,那么阴影当然可以先被独立渲染出来,不需要依赖layer本体,也就不再需要离屏渲染了。
3.3 group opacity
其实从名字就可以猜到,alpha
并不是分别应用在每一层之上,而是只有到整个layer树
画完之后,再统一加上alpha
,最后和底下其他layer
的像素进行组合。显然也无法通过一次遍历就得到最终结果。将一对蓝色和红色layer叠在一起,然后在父layer
上设置opacity
=0.5,并复制一份在旁边作对比。左边关闭group opacity
,右边保持默认(从iOS7开始,如果没有显式指定,group opacity
会默认打开),然后打开offscreen rendering
的调试,我们会发现右边的那一组确实是离屏渲染了。
3.4 mask
/ A layer whose alpha channel is used as a mask to select between the
* layer's background and the result of compositing the layer's
* contents with its filtered background. Defaults to nil. When used as
* a mask the layer's `compositingFilter' and `backgroundFilters'
* properties are ignored. When setting the mask to a new layer, the
* new layer must have a nil superlayer, otherwise the behavior is
* undefined. Nested masks (mask layers with their own masks) are
* unsupported. */
@property(nullable, strong) CALayer *mask;
我们知道mask
是应用在layer
和其所有子layer
的组合之上的,而且可能带有透明度,那么其实和group opacity
的原理类似,不得不在离屏渲染中完成(WWDC中苹果
的解释,mask
需要遍历至少三次)。
3.5 UIBlurEffect
同样无法通过一次遍历完成,其原理在WWDC
中提到:
其他还有一些,类似allowsEdgeAntialiasing
等等也可能会触发离屏渲染,原理也都是类似:如果你无法仅仅使用frame buffer
来画出最终结果,那就只能另开一块内存空间来储存中间结果。这些原理并不神秘。
3.6 光栅化
layer.shouldRasterize = YES
3.7 edge antialiasing(抗锯齿)
是否允许执行反锯齿边缘。
默认的值是 NO.(不使用抗锯齿,也有人叫反锯齿),当 Value 为YES的时候, 在layer的edgeAntialiasingMask
属性layer
依照这个值允许抗锯齿边缘,(参照这个值) 可以在info.plist
里面开启这个属性.
CALayer *layer = [CALayer layer];
layer.position = CGPointMake(100, 100);
layer.bounds = CGRectMake(0,0, 100, 100);
layer.backgroundColor = [UIColor redColor].CGColor;
//layer.allowsEdgeAntialiasing = YES;
[self.view.layer addSublayer:layer];
设置了下面
CGFloat angle = M_PI / 30.0;
[layer setTransform:CATransform3DRotate(layer.transform, angle, 0.0, 0.0, 1.0)];
继续:
layer.allowsEdgeAntialiasing = YES;
最后:
3.8 复杂形状设置圆角等
设置圆角常见的方式:
设置layer层的圆角大小.经常我们还会设置
masksToBounds
;这样做对于少量的图片,这个没有什么问题,但是数量比较多的时候,UITableView
滑动可能不是那么流畅,屏幕的帧数下降,影响用户体验。使用layer的
mask
遮罩和CAShapLayer
创建圆形的CAShapeLaer
对象,设置为View
的mask
属性,这样也可以达到圆角的效果,但是前面提到过了,使用mask属性会离屏渲染,不仅这样,还曾加了一个CAShapLayer
对象.不可取.使用带圆形的透明图片-
聪明的家伙
CoreGraphics
自定义绘制圆角.
提到CoreGraphics
,还有一种 特殊的"离屏渲染"方式 不得不提,那就是drawRect方法.触发的方式:
如果我们重写了drawRect
方法,并且使用CoreGraphics
技术去绘制.就设计到了CPU
渲染,整个渲染,由CPU
在app内同步完成,渲染之后再交给GPU显示.(这种方式对性能的影响不是很高)
//CoreGraphic通常是线程安全的,所以可以进行异步绘制,然后在主线程上更新.
- (void)displayToMaiThread {
dispatch_async(backgroundQueue, ^{
CGContextRef ctx = CGBitmapContextCreate(...);
CGImageRef img = CGBitmapContextCreateImage(ctx);
CFRelease(ctx);
dispatch_async(mainQueue, ^{
layer.contents = img;
});
});
}
3.9 渐变
3.10 重写了 drawRect 方法
4. 离屏渲染的影响
离屏渲染需要在屏幕外开辟内存空间,提前使用 CPU 渲染复杂的视图,保证视频控制器能够及时地从缓存区读到新的渲染结果。它在 GPU 面临性能瓶颈时,将压力转移一部分给比较空闲的 CPU,然而 CPU 的渲染能力远没有 GPU 高效,有点杀鸡出牛刀的意思。
同时这也是一种以空间换取时间的策略。
视频控制器要读取离屏渲染的结果,需要把渲染上下文从当前屏幕缓存区切到屏幕外缓存区,当要显示非离屏渲染视图的时候又要切换回来,然而不可能在一屏上所有的元素都是离屏渲染的,所以视频控制器上下文需要不停地来回切换。而这种上下文切换的代价非常昂贵。
所以离屏渲染会带来各方面的开销,要尽可能的避免。(鉴于离屏渲染、CPU 渲染可能带来的性能问题,一般情况下,我们要尽量使用当前屏幕渲染。)
离屏渲染并不是一无是处的,虽然会造成很多额外的开销,但也是为了充分利用设备的资源来保证界面的流畅。发生离屏渲染时,是为了引起开发者对性能的关注,减少不必要的透明视图层级。如果不可避免的要触发离屏渲染,并且发生离屏渲染视图内容不会频繁的变化,可以利用
CALayer.shouldRasterize
开启光栅化(是将几何数据经过一系列变换后最终转换为像素,从而呈现在显示设备上 的过程,光栅化的本质是坐标变换、几何离散化),将离屏渲染的内容以位图的形式缓存,减少复杂视图频繁渲染的开销。然而,这个缓存的时效是 100ms,也就是刷新 6 帧的时间,如果视图内容更新频繁,缓存就会不停的刷新,导致无法命中,开启光栅化并没有什么作用。
5. 离屏渲染的检测
5.1 模拟器 debug
-选中 color Offscreen - Renderd
离屏渲染的图层高亮成黄
可能存在性能问题
5.2 真机 Instrument
-选中 Core Animation
-勾选 Color Offscreen-Rendered Yellow
6. 结语
参与实践吧,毕竟iOS系统也不能做道十全十美,程序员也只是尽量优化,只要不是太过就好。