我们知道,图像的处理基本都是在GPU中进行,然后GPU将渲染的结果放入当前渲染屏幕的帧缓冲区中,视频控制器取出里面的内容,在屏幕上进行显示。那么有没有什么情况,会因为某些限制,GPU无法将全部的渲染结果直接写入帧缓冲区呢?
离屏渲染
因为某些原因,比如阴影、遮罩等,GPU无法将渲染结果直接写入当前渲染屏幕的帧缓冲区中,而是需要在当前屏幕帧缓冲区以外新开辟一个帧缓冲区进行渲染操作,这个过程叫做离屏渲染。
我们先来看看layer的结构:
一旦我们 为contents设置了内容 ,无论是图片、绘制内容、有图像信息的子视图等,再加上圆角+裁剪,就会触发离屏渲染。而如果contents中没有内容,加上圆角+裁剪也不会触发离屏渲染。
也就是说,当layer的contens中有东西(图片,色彩等)时, GPU为了将所有该绘画的绘画完了,再去为这个“结果”添加阴影或圆角,那么就需要离屏渲染存储layer。也就是说:阴影的本体(layer和其子layer)都还没有被组合到一起,怎么可能在第一步就画出只有完成最后一步之后才能知道的形状呢?这样一来又只能另外申请一块内存,把本体内容都先画好。
下面我们一一探究离屏渲染是否触发:
shouldRasterize 光栅化
我们使用下面的demo进行测试:
import UIKit
class ViewController: UIViewController {
lazy var imageView:UIImageView = {
let imageView = UIImageView(frame: CGRect(x:80,y:200,width:150,height:180))
imageView.contentMode = .scaleAspectFit
return imageView
}()
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(imageView)
self.imageView.image = UIImage(named: "1")
imageView.layer.shouldRasterize = true//光栅化
}
}
然后进行如下操作观察是否有离屏渲染:
这时你会发现有光栅化会触发离屏渲染 。
shouldRasterize开启后,会将layer作为位图保存下来,下次直接与其他内容进行混合。这个保存的位置就是OffscreenBuffer中。这样下次需要再次渲染的时候,就可以直接拿来使用了。
shouldRasterize使用建议:
- layer不复用,没必要打开shouldRasterize
- layer不是静态的,也就是说要频繁的进行修改,没必要使用shouldRasterize
- 离屏渲染缓存内容有100ms时间限制,超过该时间的内容会被丢弃,进而无法复用
- 离屏渲染空间是屏幕像素的2.5倍,如果超过也无法复用
遮罩Mask
添加遮罩的代码如下:
let layer = CALayer()
layer.frame = imageView.bounds
layer.backgroundColor = UIColor.red.cgColor
self.imageView.layer.mask = layer//设置遮罩
测试发现,会发生离屏渲染。
阴影
阴影代码如下:
imageView.layer.shadowColor = UIColor.red.cgColor//添加阴影
imageView.layer.shadowOffset = CGSize(width: 20, height: 20)
imageView.layer.shadowOpacity = 0.5
imageView.layer.shadowRadius = 5
测试发现会发生离屏渲染。
抗锯齿
代码如下:
self.imageView.layer.allowsEdgeAntialiasing = true
测试发现,不会发生离屏渲染。
透明度
测试代码如下:
let view = UIView(frame: CGRect(x: 0, y: 0, width: 80, height: 80))
view.backgroundColor = .red
self.imageView.addSubview(view)
self.imageView.alpha = 0.5
self.imageView.layer.allowsGroupOpacity = true
测试结果发现:会发生离屏渲染。
但是注意,如果allowsGroupOpacity设置成false,也就是不允许子视图与自己的透明度一样,这样不会触发离屏渲染,imageView的透明度(alpha)设置成1也不会触发离屏渲染。
圆角
测试代码如下:
self.imageView.backgroundColor = .red
self.imageView.layer.cornerRadius = 20
self.imageView.clipsToBounds = true
测试发现,圆角处会发生离屏渲染。
注意:然后你再创建一个label,同样操作测试:
self.view.addSubview(label)
self.label.backgroundColor = .red
self.label.layer.cornerRadius = 20
self.label.clipsToBounds = true
你会发现:label的圆角处不会触发离屏渲染。这是为什么?
这是因为我们设置imageView的backgroundColor时,设置的是layer的backgroundColor,但是,设置label的backgroundColor时,设置的是layer的contents里的backgroundColor,我们用下面的代码验证:
self.imageView.backgroundColor = .red
self.imageView.layer.backgroundColor = UIColor.green.cgColor
self.imageView.layer.cornerRadius = 20
self.imageView.clipsToBounds = true
self.view.addSubview(label)
self.label.backgroundColor = .red
self.label.layer.backgroundColor = UIColor.green.cgColor
self.label.layer.cornerRadius = 20
self.label.clipsToBounds = true
测试发现:imageView的背景颜色是green,而label的背景颜色却是red,这也验证了这一点,我们改变label中layer里的backgroundColor为绿色,但是contents中却是红色,在layer层中contents在上层,会覆盖掉下层颜色,所以还是红色。
设置圆角时,只会设置backgroundColor和border,要触发离屏渲染,我们需要操作contents。
离屏渲染的劣势:
离屏渲染增大了系统的负担,会形象App性能。主要表现在以下几个方面:
- 离屏渲染需要额外的存储空间,渲染空间大小的上限是2.5倍的屏幕像素大小,超过无法使用离屏渲染
- 容易掉帧:一旦因为离屏渲染导致最终存入帧缓存区的时候,已经超过了16.67ms,则会出现掉帧的情况,造成卡顿
优化:
- 贝塞尔曲线绘制圆角
- 如果产品设计圆角+阴影的卡片,可以使用切图实现圆角+阴影,避免触发离屏渲染,这也是最优解。