本文关注如下几个问题
- iOS 平台上离屏渲染的概念?
- 为什么需要离屏渲染?
- 离屏渲染到底好还是不好?能避免离屏渲染吗?如何优化?
之前聊聊iOS渲染 (opens new window)这篇文章里介绍了正常情况下渲染视图的架构方式,如图,那什么是离屏渲染?
# 离屏渲染
离屏渲染这个概念不光是 iOS 独有的,Windows 上也有类似的概念 (opens new window)。
离屏渲染的过程是**「GPU 因为一些原因,没办法把渲染出来的结果直接输出给帧缓冲器(frame buffer),而是先暂存在别的内存区域,之后再写入帧缓冲区」。**
即正常流程是这样子的
离屏渲染后
一个误区:我们通过在 drawRect 里面进行的绘制,并不是离屏渲染,那儿的绘制主要是通过 Quartz2D 图形库在 CGContext 维护的内存(backing store)里进行的绘制。距离 UIKit 工程师说的真正的 GPU 离屏渲染还差的远呢。参考 即刻技术-关于iOS离屏渲染的深入研究 (opens new window)
# 为什么需要离屏渲染
先说正常的渲染方式,在上篇文章 (opens new window)里我们提到一个重要的渲染过程的参与者「渲染服务器」,主要的渲染操作就是渲染服务器拿到 CoreAnimation 提交的图层树(layer tree)并解码后,通过 OpenGL 或者 Metal 提交命令去让 GPU 渲染对应的图层。
具体的渲染方法是参考「画家算法」,GPU 一层层的渲染,并且将渲染结果放到帧缓冲器里,后面绘制结果会覆盖掉前面的结果,而每一层绘制完之后这一层的内容就会被丢弃。这意味着整个渲染的过程是不可逆的。
PS:没找到 iOS 官方说明的渲染方法,可能画家算法是业内通用的 GPU 绘制方式?
需要离屏渲染的原因就是普通的画家算法不能满足一些绘图的场景,比如下图这种互相遮挡的绘图场景,我应该先绘制谁呢?好像先绘谁也不好使。
# 离屏渲染到底好还是不好?能避免离屏渲染吗?
离屏渲染相对于普通的渲染流程来说肯定是更加消耗资源的,需要单独分配和管理内存,而且打乱了原始的 GPU 设计好的渲染管线流程。最后将内存中的 OffscreenFrameBuffer 拷贝到待显示的 FrameBuffer 这一步也是消耗 CPU 资源的。
但是有的时候离屏渲染是不可避免的,有一些效果就是需要离屏渲染才能做到。这时候我们能做的就是尽量降低离屏渲染带来的性能损耗。
CALayer
提供了一种优化方案就是「光栅化」。CALayer
提供了属性 shouldRasterize
来让开发者设置光栅化开关。光栅化的效果是让渲染服务器(render server)将layer的渲染结果缓存起来,这样被缓存起来的内容可以再进行复用,从而减少离屏渲染次数,提升性能。
光栅化是指将基于矢量的图像或对象转换为位图格式的过程。它涉及将形状和线条的数学描述转换为像素网格。
当然如果 CALayer 的其他属性修改影响了最终的渲染效果,之前缓存好的内容会被丢弃。所以优化离屏渲染的最佳场景是 CALayer 为静态内容的情况。
iOS 模拟器提供了识别离屏渲染机制的方式,通过打开模拟器的 Debug → Color Off-screen Rendered
的选项,就能查看当前视图是否被离屏渲染了。
Color Off-screen Rendered-Simulator overlays content in yellow that is rendered offscreen. Only use off-screen rendering when you have confirmed with tests that the memory and performance tradeoffs work for your app.
我们知道 CALayer
的一些常见属性是能触发离屏渲染的
阴影(shadow)相关的属性。
btn.layer.shadowOffset = CGSize.init(width: 20, height: 20) btn.layer.shadowColor = UIColor.black.cgColor btn.layer.shadowRadius = 5 btn.layer.shadowOpacity = 1
在模拟器上展示效果如下,可以看到整个视图都被黄色覆盖,说明视图被离屏渲染了。
但是通过配置 shadowPath 属性可以避免离屏渲染
var scale = CGAffineTransform.identity btn.layer.shadowPath = CGPath.init(rect: .init(x: 20, y: 20, width: 200, height: 60), transform: &scale) btn.layer.shadowColor = UIColor.black.cgColor btn.layer.shadowOpacity = 0.5 btn.layer.shadowRadius = 5
**透明度(opacity)相关属性。**多说一句,
UIView
的alpha
属性设置底层就是通过Layer
的opacity
实现的,所以这俩属性我理解除了语义上的区别之外,没啥差别。设置UIView
alpha
属性同样能触发离屏渲染的。这种透明度的设置是对整个子图层树都生效的,所以我理解这也是它需要离屏渲染的原因。有争议的圆角属性
cornerRadius
属性。最近的 iOS 系统中使用cornerRadius
并不会触发离屏渲染。但一些使用场景需要注意cornerRadius
本身并不会触发离屏渲染,但是如果结合masksToBounds
属性的化就会触发离屏渲染,如图四个角会被标记为黄色。有背景色也可以使用
cornerRadius
。因为之前看即刻的技术文章 (opens new window)说,没有backgroundColor
可以使用cornerRadius
。我自己理解就算有backgoundColor
也能用,但得知道是设置谁的backgroundColor
。看下面代码,当 ① ③ 组合的时候,
cornerRadius
并不对背景色生效;当 ② ③ 组合的时候cornerRadius
是对背景色生效。① label.backgroundColor = UIColor.gray ② label.layer.backgroundColor = UIColor.gray.cgColor ③ label.layer.cornerRadius = 10
因为
UIView
的backgoundColor
本质上是给CALayer
的contents
属性填充内容。CALayer
的backgoundColor
和contents
属性不一样的。cornerRadius
作用的是CALayer
的backgoundColor
和border
,而不对contents
属性生效。上面两种都不会触发离屏渲染。有一些文章推荐使用 CoreGraphics 的方式来进行圆角绘制,如下
override func draw(_ rect: CGRect) { let context = UIGraphicsGetCurrentContext() let path = UIBezierPath(roundedRect: rect, cornerRadius: rect.height / 2) context?.setFillColor(UIColor.gray.cgColor) path.fill() }
不过我自己认为既然
cornerRadius
已经不会触发离屏渲染了,是否还有必要提前绘制圆角呢?这种场景我理解就是将绘制的过程前置让 CPU 提前对视图做了预处理,这样 GPU 就不用再做了。
最后关于调试离屏渲染,离屏渲染消耗的时间并不会出现在 Instrument 的 TimeProfiler 工具里,因为这一层的渲染是 CoreAnimation 的 RenderServer 和 GPU 配合做的,和我们的 App并没有直接的关系。
参考地址:
- 知乎-离屏渲染 (opens new window)
- objcio-绘制像素到屏幕上 (opens new window)
- Drawing Offscreen-OpenGL Programming Guide for Mac (opens new window)
- 即刻技术-关于iOS离屏渲染的深入研究 (opens new window)
- Core Animation Programming Guide (opens new window)
- Quartz 2D Programming Guide (opens new window)
- WWDC14:Advanced Graphics and Animations for iOS Apps (opens new window)
- WWDC14: Advanced Graphics and Animations for iOS Apps 字幕版 (opens new window)
- [WWDC 2012 Polishing Your Interface Rotations](https://archive.org/details/wwdc-2012-sessions/[2012]+[Session+240]+Polishing+Your+Interface+Rotations.mov](https://archive.org/details/wwdc-2012-sessions/%5B2012%5D+%5BSession+240%5D+Polishing+Your+Interface+Rotations.mov)