离屏渲染(OffScreen Rendering)
这个概念对于iOS开发者来讲并不陌生,对App的性能优化和面试中不止一次的遇到,今天咱们再来聊一聊这个问题。ios
原本是想写在上一篇 iOS下的图像渲染原理 中的,感受篇幅有点长了,影响阅读体验,因此单写了一篇。面试
在讨论离屏渲染
以前,咱们先来看看正常的渲染逻辑。算法
这里省略了其余的渲染细节。GPU 以60 FPS
的帧率将渲染结果存储到帧缓冲区(Frame Buffer)
, 屏幕把每一帧图像以60 Hz
的频率刷新显示。缓存
那么离屏渲染
的大体流程是是什么样的呢?性能优化
若是有时由于面临一些限制,没法把渲染结果直接写入Frame Buffer
,而是先暂存在另外的内存区域,以后再写入Frame Buffer
,那么这个过程被称之为离屏渲染
。markdown
经过上一篇iOS下的图像渲染原理的讲解,咱们知道主要的渲染操做都是由 GPU CoreAnimation
的Render Server
模块,经过调用显卡驱动所提供的OpenGL/Metal
接口来执行的。多线程
对于每一层layer,Render Server
会遵循画家算法
,按次序输出到Frame Buffer
,后一层覆盖前一层,就能获得最终的显示结果。函数
可是有一些状况下,并无这么简单。GPU 的Render Server
遵循画家算法
,一层一层的进行输出,可是在某一层渲染完成后,没法在回过头来处理或者改变其中的某个部分,由于在这以前的全部层像素数据,已经在渲染结束后被永久覆盖/丢弃了。post
若是咱们想对某一层layer进行叠加/裁剪或者其余复杂的操做,就不得不新开一块内存区域,来处理这些些更加复杂的操做。性能
咱们看过一些文章有提到过CPU离屏渲染
,那么什么是CPU离屏渲染
呢?
若是咱们在UIView
中实现了-drawRect
方法,就算它的函数体内部实际没有代码,系统也会为这个view
申请一块内存区域,等待CoreGraphics
可能的绘画操做。
这种状况下,新开辟了一块CGContext
,渲染数据暂时存储在了CGContext
中,而没有给到Frame Buffer
。根据上面的定义来讲,没有把渲染结果直接给到Frame Buffer
的,那就属于离屏渲染
了。
可是,全部 CPU 执行的光栅化操做,好比图片的解码等等,都没法直接绘制到 GPU 的Frame Buffer
中,多须要一块用来中转的内存区域。固然,咱们知道 CPU 并不擅长渲染,因此咱们应该尽可能避免使用 CPU 渲染。根据苹果的说法,这并不是真正意义上的离屏渲染
,而且若是咱们重写了-drawRect
方法,使用Xcode
检测,也并不会被标记为离屏渲染
。
在模拟器中经过设置 Color Off-Screen Rendered
来检查哪些图层触发了离屏渲染。
触发了离屏渲染的图层会被标记为黄色。
当须要裁切图层的内容content
,很显然这就须要开辟一块内存来操做了。当只设置cornerRadius
时,不须要裁切内容,只须要一个带圆角的边框,则不会触发离屏渲染。
阴影依赖layer自己的形状等信息,而且根据画家算法
,阴影须要先画出来,这样来讲就须要在单独的内存中先进行依赖的合成计算,再添加到Frame Buffer
,形成离屏渲染
。不过若是咱们可以预先告诉CoreAnimation
(经过shadowPath
属性)阴影的几何形状,那么阴影固然能够先被独立渲染出来,不须要依赖layer本体,也就再也不须要离屏渲染
了。
须要将一组图层画完以后,再总体加上alpha
,最后和底下其余layer的像素进行组合。显然也没法经过一次遍历就获得最终结果。
咱们知道mask是应用在layer和其全部子layer的组合之上的,并且可能带有透明度,那么其实和group opacity
的原理相似,不得不在离屏渲染中完成。
渲染出毛玻璃效果,须要先画出原图层,而后capture
原图层,进行水平模糊(Horizontal Blur)和垂直模糊(Vertical Blur),最后进行合成操做。显然这须要在离屏缓冲区中完成。
shouldRasterize
一旦被设置为YES
,Render Server
就会强制把layer的渲染结果(包括其子layer,以及圆角、阴影、group opacity
等等)保存在一块内存中,这样一来在下一帧仍然能够被复用,而不会再次触发离屏渲染。有几个须要注意的点
shouldRasterize
的主旨在于下降性能损失,但老是至少会触发一次离屏渲染。若是你的layer原本并不复杂,也没有圆角阴影等等,则没有必要打开shouldRasterize
shouldRasterize
,把layer绘制到一块缓存,而后在接下来复用这个结果,这样就不须要每次都从新绘制整个layer树了2.5倍
大小,若是超出了会自动被丢弃,且没法被复用了100ms
没有被使用,会自动被丢弃,且没法被复用了shouldRasterize
反而影响效率了若是你没法仅仅使用Frame Buffer
来画出最终结果,那就只能另开一块内存空间来储存中间结果。
一般状况下,咱们会使用 cornerRadius
来设置圆角
view.layer.cornerRadius = 50; 复制代码
咱们看过不少文章都在说单独使用 cornerRadius
是不会触发离屏渲染的,先来实现一个很是简单圆角Button,只设置了backgroundColor
,没有setImage:
UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom]; btn.frame = (CGRect){ .origin.x = 100, .origin.y = 280, .size.width = 100, .size.height = 100, }; btn.backgroundColor = [UIColor blueColor]; btn.layer.cornerRadius = 50; [self.view addSubview:btn2]; 复制代码
此时的确不会触发离屏渲染,也达到了圆角的目的。
在实际项目中,通常会使用一张图片做为Button
或者ImageView
的背景,这样若是只设置cornerRadius
,是达不到圆角的效果的,还须要设置masksToBounds = YES
imageView.layer.cornerRadius = 50; imageView.layer.masksToBounds = YES; 复制代码
这里用UIImageView
来举例,咱们看下效果
UIImageView *imageView = [[UIImageView alloc] init]; imageView.frame = (CGRect){ .origin.x = 100, .origin.y = 400, .size.width = 100, .size.height = 100, }; imageView.layer.cornerRadius = 50; imageView.layer.masksToBounds = YES; imageView.image = [UIImage imageNamed:@"btn.png"]; [self.view addSubview:imageView]; 复制代码
能够看到,这里仍然没有发生离屏渲染。那么离屏渲染到底和什么有关系呢?
仍是上面的UIImageView
的案例,咱们尝试设置一下它的backgroundColor
UIImageView *imageView = [[UIImageView alloc] init]; imageView.frame = (CGRect){ .origin.x = 100, .origin.y = 400, .size.width = 100, .size.height = 100, }; imageView.layer.cornerRadius = 50; imageView.layer.masksToBounds = YES; imageView.backgroundColor = [UIColor blueColor]; imageView.image = [UIImage imageNamed:@"btn.png"]; [self.view addSubview:imageView]; 复制代码
在同时设置了backgroundColor
和setImage:
以后,这里触发了离屏渲染。
关于性能优化,就是平衡 CPU 与 GPU 的负载工做,由于要作的事情就那么多。当 GPU 忙不过来的时候,咱们能够利用 CPU 的空闲来渲染而后提交给 GPU 显示,来提升总体的渲染效率。渲染不是CPU的强项,调用CoreGraphics
会消耗其至关一部分计算时间,通常来讲 CPU 渲染都在后台线程完成(这也是AsyncDisplayKit
的主要思想),而后再回到主线程上,把渲染结果传回CoreAnimation
。这样一来,多线程间数据同步会增长必定的复杂度。CPU渲染速度不够快,所以只适合渲染静态的元素,如文字、图片。做为渲染结果的bitmap数量较大,很容易致使OOM。若是你选择使用 CPU 来作渲染,那么就没有理由再触发 GPU 的离屏渲染了。