On-Screen Rendering:当前屏幕渲染,CPU、GPU 不停地将内容渲染完成放入frame buffer帧缓冲区中,显示屏幕从 frame buffer中获取内容显示。

Off-Screen Rendering:离屏渲染,先创建离屏渲染帧缓冲区offscreen frame buffer,然后逐一将内容渲染放入其中,完成后对离屏渲染缓冲区做阴影叠加、裁剪等操作,最后将结果拷贝或切换到帧缓冲区frame buffer中,显示屏幕从 frame buffer中获取内容显示。

为什么需要

那么为什么需要离屏渲染帧缓冲区offscreen frame buffer呢?我们先来了解下“画家算法”。

画家算法,也叫作优先填充,它是三维计算机图形学中处理可见性问题的一种解决方法(三维场景投影到二维平面)。如下图,画家算法首先将场景中的多边形根据深度进行排序,然后按照由远到近的顺序进行描绘,这种方法通常会将不可见的部分覆盖,这样就可以解决可见性问题。

”画家算法“,每一层由远到近依次输出到画布

对于有前后依赖的图层(如阴影叠加、裁剪等),通过由远到近的图层叠加算法是无法实现的,我们需要先申请一个临时缓冲区,所有图层按照画家算法,由远到近在临时缓冲区渲染,渲染完成后,再对这个临时缓冲区做最后的全局操作(如阴影叠加、裁剪等),最后再把临时缓冲区拷贝或切换到当前的缓冲区上,交给显示器显示。

总结一下,使用离屏渲染大概是因为以下原因:

  1. 需要实现特殊的效果,比如说全局叠加、裁剪等等,需要用额外的帧缓冲区offscreen frame buffer保存中间状态。

  2. 出于效率目的,针对不会经常变更的图层,可以缓存到offscreen frame buffer,供下次刷新使用。

什么时候出现

Masking

最常见的情形就是使用了Masking蒙版,我们看下官方提供的Masking渲染流程:

如上图,渲染Masking蒙版分为3步:

  1. 渲染 layermask 纹理,流程同 Tile Based Rendering
  2. 渲染 layercontent 纹理,流程同 Tile Based Rendering
  3. 合成操作,合并 maskcontent 纹理。

由于需要叠加两层渲染结果,所以在叠加前,需要额外的缓冲区保存渲染结果,也就是触发了离屏渲染。

UIVisualEffectView

iOS 8 提供的blur模糊特效也会引起离屏渲染,我们看下官方提供的blur渲染流程:

如上图,渲染模糊过程分为4步:

  1. 渲染 layercontent
  2. 截获 layercontent,进行缩放。
  3. 对缩放内容进行横向模糊。
  4. 对缩放内容进行纵向模糊。
  5. 合成操作,合并所有模糊结果。

blur模糊效果也会触发离屏渲染,而且需要更多的缓冲区存储渲染结果,更浪费性能。

Rasterization

光栅化,开启光栅化后,会触发离屏渲染,GPU 会强制把图层的渲染结果保存下来,方便下次复用。我们看下官方的描述:

光珊化的本意是为了避免静态内容重绘、复杂view层级重绘带来的影响。使用需要注意以下几点:
这里有一个iOS交流圈:891 488 181 有兴趣的都可以来了解,分享BAT,阿里面试题、面试经验,讨论技术,裙里资料直接下载就行, 大家一起交流学习成长!

  1. CALayer 的 shouldRasterize 可开启光珊化。
  2. 如果 layer 不是静态,或者频繁修改(动画中),开启光珊化反而影响效率。
  3. 不要过度使用,光珊化离屏渲染缓存大小限制为2.5倍屏幕大小。
  4. 光珊化离屏渲染缓存100ms没有被使用,就会被丢弃。

Group Opactiy

组不透明度,某些情况也会触发离屏渲染,可以通过CALayerallowsGroupOpacity控制。我们看官方的描述:

总结一下,有以下两点:

  1. 建议关闭CALayerallowsGroupOpacity 属性。

  2. 如果开启了 allowsGroupOpacity,当 layer 的 opacity 小于1.0,且有子 layer 或者背景图,则会触发离屏渲染。

而在iOS 7.0 SDK以上,allowsGroupOpacity 默认 true,所以不需要的时候,建议关闭。

Shadow

投影,和Masking一样,涉及到多个渲染结果合并,也会启用离屏渲染。来看下官方的解释:

如果单纯设置shadowOffset,会触发离屏渲染,但是我们可以设置shadowPath,告诉Core Animation投影路径,那么系统就知道如何绘制投影了,就不会触发离屏渲染了。

圆角

通常我们通过layer.cornerRadius设置圆角,只会默认设置layer backgroundColorborder的圆角,同时结合layer.masksToBoundscontent设置圆角。

单纯的cornerRadius+masksToBounds不一定会产生离屏渲染,如果这个圆角裁剪操作需要作用多个图层,也就是layer上有其他图层,那么肯定会发生离屏渲染。比如以下两种情况:

  1. layer 包含 sub layer,或者有 content,会触发离屏渲染。
  2. 针对 UIImageView,同时设置了 backgroundColorimage,会触发离屏渲染。

测试圆角离屏渲染

比如以下代码,运行后,打开Xcode -> Debug -> View Debuging -> Rendering -> Color Offscreen Rendered Yellow开关,查看离屏渲染情况:

let view = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
view.layer.cornerRadius = 2
view.layer.masksToBounds = true
self.view.addSubview(view)

// 1.设置 layer.contents ,触发离屏渲染
view.layer.contents = UIImage(named: "bc_delete")?.cgImage

// 2.addSubview ,触发离屏渲染
let textLab = UILabel(frame: CGRect(x: 20, y: 20, width: 40, height: 40))
textLab.text = "test"
view.addSubview(textLab)

// 3\. UIImageView同时设置image、backgroundColor,触发离屏渲染
let view = UIImageView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
view.image = UIImage(named: "bc_delete")
view.backgroundColor = UIColor.gray
view.layer.cornerRadius = 20
view.layer.masksToBounds = true
self.view.addSubview(view)

避免圆角离屏渲染

那么如何避免圆角裁剪产生的离屏渲染呢?我们先来看官方描述:

总结一下:

  1. 不要使用不必要的 mask,可以预处理图片为圆形,通过 Core Graphics手动绘制圆角。

  2. 使用中间为圆形透明的白色背景视图覆盖。但这种方式不能解决背景色为图片或渐变色的情况。

  3. UIBezierPath 贝塞尔曲线绘制闭合带圆角的矩形,再将不带圆角的 layer 渲染成图片,添加到贝塞尔矩形中。这种方法效率更高,但是 layer 的布局一旦改变,贝塞尔曲线都需要手动地重新绘制,稍微麻烦。

总结

最后我们总结一下,常见触发离屏渲染的情况有以下6种:

  1. layer 设置了使用了 mask 蒙版。

  2. layer 设置了圆角裁剪(masksToBounds+cornerRadius),且包含 sub layer。

  3. layer 设置了组不透明度allowsGroupOpacity,并且不透明度opacity小于1。

  4. layer 设置了投影shadowXX

  5. layer 设置了光栅化shouldRasterize

  6. 使用了 blur 模糊效果UIVisualEffectView

另外,我们可以通过打开Xcode -> Debug -> View Debuging -> Rendering -> Color Offscreen Rendered Yellow开关,检查离屏渲染情况。

参考链接

博客原文

WWDC14 419

Apple Core Animation Programming Guide

iOS 渲染原理解析

关于iOS离屏渲染的深入研究

文章到这里就结束了,如果你有什么意见和建议欢迎给我留言。你可以加我及时获取最新资料。
作者:老青菜
链接:https://juejin.cn/post/6901973358657667085