iOS中的离屏渲染(Off-Screen Rendering)

2020/7/9 23:08:48

本文主要是介绍iOS中的离屏渲染(Off-Screen Rendering),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

ios下的离屏渲染

离屏渲染(OffScreen Rendering) 这个概念对于iOS开发者来说并不陌生,对App的性能优化和面试中不止一次的遇到,今天我们再来聊一聊这个问题。

本来是想写在上一篇 iOS下的图像渲染原理 中的,感觉篇幅有点长了,影响阅读体验,所以单写了一篇。

什么是离屏渲染

在讨论离屏渲染之前,我们先来看看正常的渲染逻辑。

frame-buffer

这里省略了其他的渲染细节。GPU 以60 FPS的帧率将渲染结果存储到帧缓冲区(Frame Buffer), 屏幕把每一帧图像以60 Hz的频率刷新显示。

那么离屏渲染的大致流程是是什么样的呢?

off-screen-buffer

如果有时因为面临一些限制,无法把渲染结果直接写入Frame Buffer,而是先暂存在另外的内存区域,之后再写入Frame Buffer,那么这个过程被称之为离屏渲染

GPU 离屏渲染

通过上一篇iOS下的图像渲染原理的讲解,我们知道主要的渲染操作都是由 GPU CoreAnimationRender Server模块,通过调用显卡驱动所提供的OpenGL/Metal接口来执行的。

对于每一层layer,Render Server会遵循画家算法,按次序输出到Frame Buffer,后一层覆盖前一层,就能得到最终的显示结果。

但是有一些情况下,并没有这么简单。GPU 的Render Server遵循画家算法,一层一层的进行输出,但是在某一层渲染完成后,无法在回过头来处理或者改变其中的某个部分,因为在这之前的所有层像素数据,已经在渲染结束后被永久覆盖/丢弃了。

如果我们想对某一层layer进行叠加/裁剪或者其他复杂的操作,就不得不新开一块内存区域,来处理这些些更加复杂的操作。

CPU 离屏渲染

我们看过一些文章有提到过CPU离屏渲染,那么什么是CPU离屏渲染呢?

如果我们在UIView中实现了-drawRect方法,就算它的函数体内部实际没有代码,系统也会为这个view申请一块内存区域,等待CoreGraphics可能的绘画操作。

这种情况下,新开辟了一块CGContext,渲染数据暂时存储在了CGContext中,而没有给到Frame Buffer。根据上面的定义来说,没有把渲染结果直接给到Frame Buffer的,那就属于离屏渲染了。

但是,所有 CPU 执行的光栅化操作,比如图片的解码等等,都无法直接绘制到 GPU 的Frame Buffer 中,多需要一块用来中转的内存区域。当然,我们知道 CPU 并不擅长渲染,所以我们应该尽量避免使用 CPU 渲染。根据苹果的说法,这并非真正意义上的离屏渲染,并且如果我们重写了-drawRect方法,使用Xcode检测,也并不会被标记为离屏渲染

如何检测项目中哪些图层触发了离屏渲染?

在模拟器中通过设置 Color Off-Screen Rendered 来检查哪些图层触发了离屏渲染。

模拟器offscreen-rendering检查

触发了离屏渲染的图层会被标记为黄色。

模拟器offscreen-rendering演示

触发离屏渲染的场景

通过设置cornerRadius与masksToBounds达到圆角裁切效果

当需要裁切图层的内容content,很显然这就需要开辟一块内存来操作了。当只设置cornerRadius时,不需要裁切内容,只需要一个带圆角的边框,则不会触发离屏渲染。

shadow

阴影依赖layer本身的形状等信息,并且根据画家算法,阴影需要先画出来,这样来说就需要在单独的内存中先进行依赖的合成计算,再添加到Frame Buffer,造成离屏渲染。不过如果我们能够预先告诉CoreAnimation(通过shadowPath属性)阴影的几何形状,那么阴影当然可以先被独立渲染出来,不需要依赖layer本体,也就不再需要离屏渲染了。

group opacity

需要将一组图层画完之后,再整体加上alpha,最后和底下其他layer的像素进行组合。显然也无法通过一次遍历就得到最终结果。

mask

我们知道mask是应用在layer和其所有子layer的组合之上的,而且可能带有透明度,那么其实和group opacity的原理类似,不得不在离屏渲染中完成。

UIBlurEffect

渲染出毛玻璃效果,需要先画出原图层,然后capture原图层,进行水平模糊(Horizontal Blur)和垂直模糊(Vertical Blur),最后进行合成操作。显然这需要在离屏缓冲区中完成。

shouldRasterize

shouldRasterize一旦被设置为YESRender Server就会强制把layer的渲染结果(包括其子layer,以及圆角、阴影、group opacity等等)保存在一块内存中,这样一来在下一帧仍然可以被复用,而不会再次触发离屏渲染。有几个需要注意的点

  • shouldRasterize的主旨在于降低性能损失,但总是至少会触发一次离屏渲染。如果你的layer本来并不复杂,也没有圆角阴影等等,则没有必要打开shouldRasterize
  • 如果layer的子结构非常复杂,渲染一次所需时间较长,可以打开shouldRasterize,把layer绘制到一块缓存,然后在接下来复用这个结果,这样就不需要每次都重新绘制整个layer树了
  • 离屏渲染缓存有空间上限,最多不超过屏幕总像素的2.5倍大小,如果超出了会自动被丢弃,且无法被复用了
  • 离屏渲染缓存内容有时间限制,一旦缓存超过100ms没有被使用,会自动被丢弃,且无法被复用了
  • 如果layer不是静态的,需要被频繁修改,比如处在动画之中,那么开启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];
复制代码

模拟器offscreen-rendering演示2

此时的确不会触发离屏渲染,也达到了圆角的目的。

在实际项目中,一般会使用一张图片作为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];
复制代码

模拟器offscreen-rendering演示3

可以看到,这里仍然没有发生离屏渲染。那么离屏渲染到底和什么有关系呢?

还是上面的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];
复制代码

模拟器offscreen-rendering演示4

在同时设置了backgroundColorsetImage:之后,这里触发了离屏渲染。

总结

关于性能优化,就是平衡 CPU 与 GPU 的负载工作,因为要做的事情就那么多。当 GPU 忙不过来的时候,我们可以利用 CPU 的空闲来渲染然后提交给 GPU 显示,来提高整体的渲染效率。渲染不是CPU的强项,调用CoreGraphics会消耗其相当一部分计算时间,一般来说 CPU 渲染都在后台线程完成(这也是AsyncDisplayKit的主要思想),然后再回到主线程上,把渲染结果传回CoreAnimation。这样一来,多线程间数据同步会增加一定的复杂度。CPU渲染速度不够快,因此只适合渲染静态的元素,如文字、图片。作为渲染结果的bitmap数量较大,很容易导致OOM。如果你选择使用 CPU 来做渲染,那么就没有理由再触发 GPU 的离屏渲染了。



这篇关于iOS中的离屏渲染(Off-Screen Rendering)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程