离屏渲染
什么是离屏渲染
正常情况下 , 渲染完成的内容放在帧缓存区(Framebuffer) , 屏幕不断的从中Framebuffer获取内容并展示 .
离屏渲染指的是在正常情况下额外创建了离屏渲染缓存区(Offscreenbuffer) , 现将提前渲染的内容存放在了Offscreenbuffer中 , 最后再将离屏渲染缓存区的内容进行叠加完成后存入Framebuffer中 .
离屏渲染会有什么问题
-
内存问题 : 因为需要额外的创建Offscreenbuffer , 如果存在大量的离屏渲染时,势必会造成内存的压力 . Offscreenbuffer的大小也是有限制的 (不能超过屏幕总像素的2.5倍) .
-
掉帧问题: 因为需要额外创建**Offscreenbuffer **, 提前渲染. 并最终进行叠加处理和Framebuffer进行内容切换.整个过程相较于正常渲染流程要更耗时 . 存在大量离屏渲染的情况下 , 很容易造成掉帧 , 导致屏幕卡顿 .
触发离屏渲染的原因
我个人的理解就是, 一次绘制不能完成, 需要保存中间状态, 最后进行叠加处理情况就会触发离屏渲染. 比如我们开发中常用到可能会触发离屏渲染情况: 遮罩处理, 圆角处理, 阴影处理等.
为什么说可能呢, 我们通过代码示例来分析:
阴影的处理
override func viewDidLoad() {
super.viewDidLoad()
let view = UIView.init(frame: CGRect.zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = UIColor.red
self.view.addSubview(view)
self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-100-[view(==200)]", options: [], metrics: nil, views: ["view" : view]))
self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-100-[view(==200)]", options: [], metrics: nil, views: ["view" : view]))
view.layer.shadowOffset = CGSize.init(width: 10, height: 10)
view.layer.shadowColor = UIColor.black.cgColor
view.layer.shadowOpacity = 1.0
}
打开模拟器Debug里的off-screen调试可以看到的确触发了离屏渲染 (出现了黄色的图层就表示触发了离屏渲染), 如下图 :
![](https://img.haomeiwen.com/i3096223/c7f9404e1cae591a.png)
设置阴影避免触发离屏渲染: 使用shadowPath
view.layer.shadowPath = UIBezierPath.init(rect: CGRect.init(x: 0, y: 0, width: 200, height: 200)).cgPath
view.layer.shadowOffset = CGSize.init(width: 10, height: 10)
view.layer.shadowColor = UIColor.black.cgColor
view.layer.shadowOpacity = 1.0
![](https://img.haomeiwen.com/i3096223/b2052ead6740377b.png)
圆角的处理
创建一个view, 设置圆角, 并打开 masksToBounds
let view = UIView.init(frame: CGRect.zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = UIColor.red
self.view.addSubview(view)
self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-100-[view(==200)]", options: [], metrics: nil, views: ["view" : view]))
self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-100-[view(==200)]", options: [], metrics: nil, views: ["view" : view]))
view.layer.cornerRadius = 100
view.layer.masksToBounds = true
![](https://img.haomeiwen.com/i3096223/1209e1cb4b5f210a.png)
由图可见, 并没有触发离屏渲染. 我们给view添加一个子view看看呢.
let subView = UIView.init(frame: CGRect.init(x: 0, y: 0, width: 100, height: 100))
subView.backgroundColor = UIColor.red
view.addSubview(subView)
![](https://img.haomeiwen.com/i3096223/5d98dfc4bcdcd644.png)
可见, 当存在子视图的时候, 对父视图做圆角处理, 打开masksToBounds裁剪的时候会触发离屏渲染.
对于UIImageView
imageView.layer.cornerRadius = 100
imageView.layer.masksToBounds = true
imageView.contentMode = .scaleAspectFill
imageView.image = UIImage.init(named: "timg.jpeg")
![](https://img.haomeiwen.com/i3096223/643bf0c7ccae8991.png)
iOS9.0 之后单纯的设置圆角, UIImageView并不会触发离屏渲染. 但是如果我们加上了
边框
或者背景色
效果就不一样了.
// 加上边框
imageView.layer.borderWidth = 2.0
imageView.layer.borderColor = UIColor.black.cgColor
![](https://img.haomeiwen.com/i3096223/6d389e0cc5103102.png)
// 加上背景色
imageView.backgroundColor = .cyan
![](https://img.haomeiwen.com/i3096223/7ad96020df8f0595.png)
可见当UIImageView存在背景色或者边框的时候进行圆角处理会触发离屏渲染.
之所以会有上面所述的差异, 是因为当layer存在sublayer(比如: 存在子视图)
或者content不为空(比如: UIImageView的image不为空)
时, 同时设置了backgroundColor或者border进行裁剪时就会触发离屏渲染.
我们可以看下layer的层级结构:
切圆角产生离屏渲染流程图: 下图非原创, 引用出处点我跳转
)
那么如何尽量避免离屏渲染呢, 可行的实现方法大概有下面几种:
此处文字引用自出处: 点我跳转
- 【换资源】直接使用带圆角的图片,或者替换背景色为带圆角的纯色背景图,从而避免使用圆角裁剪。不过这种方法需要依赖具体情况,并不通用。
- 【mask】再增加一个和背景色相同的遮罩 mask 覆盖在最上层,盖住四个角,营造出圆角的形状。但这种方式难以解决背景色为图片或渐变色的情况。
- 【UIBezierPath】用贝塞尔曲线绘制闭合带圆角的矩形,在上下文中设置只有内部可见,再将不带圆角的 layer 渲染成图片,添加到贝塞尔矩形中。这种方法效率更高,但是 layer 的布局一旦改变,贝塞尔曲线都需要手动地重新绘制,所以需要对 frame、color 等进行手动地监听并重绘。
- 【CoreGraphics】重写 drawRect:,用 CoreGraphics 相关方法,在需要应用圆角时进行手动绘制。不过 CoreGraphics 效率也很有限,如果需要多次调用也会有效率问题。
对于第一种我们就不讨论, 先来看看第二种方式使用mask遮罩
在网上看了大部分的文章都是采用下面的代码:
let radius: CGFloat = 100
// 父视图
let view = UIView.init(frame: CGRect.zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .green
self.view.addSubview(view)
// 拉约束
self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-100-[view(==200)]", options: [], metrics: nil, views: ["view" : view]))
self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-100-[view(==200)]", options: [], metrics: nil, views: ["view" : view]))
// 添加子视图
let subView = UIView.init(frame: CGRect.init(x: 0, y: 0, width: 100, height: 100))
subView.backgroundColor = UIColor.purple
view.addSubview(subView)
// 圆角
let bPath = UIBezierPath.init(arcCenter: CGPoint.init(x: radius, y: radius), radius: radius, startAngle: 0, endAngle: CGFloat.pi * 2, clockwise: true)
let shapeLayer = CAShapeLayer.init()
shapeLayer.frame = CGRect.init(x: 0, y: 0, width: radius * 2, height: radius * 2)
shapeLayer.path = bPath.cgPath
view.layer.mask = shapeLayer
采用的是设置layer的mask的方案, 但是以我个人的理解, 我觉得这样设置遮罩应该还是会触发离屏渲染的. 我运行调试的时候发现, 的确会有离屏渲染, 调试出现了黄色:
![](https://img.haomeiwen.com/i3096223/a9bf62952abc86fe.png)
我想的方案是使用addSublayer的方式, 创建四个角, 添加到主layer中.
func addCorner(_ radius: CGFloat, _ view: UIView) {
func createLayer(frame: CGRect, center: CGPoint, raduis: CGFloat, startAngle: CGFloat, endAngle: CGFloat, startPoint: CGPoint, endPoint: CGPoint) -> CALayer {
let layer = CAShapeLayer.init()
let bPath = UIBezierPath.init()
layer.frame = frame
bPath.move(to: startPoint)
bPath.addArc(withCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
bPath.addLine(to: endPoint)
bPath.close()
layer.path = bPath.cgPath
layer.fillColor = UIColor.white.cgColor
return layer
}
// 左上角
view.layer.addSublayer(createLayer(
frame: CGRect.init(x: 0, y: 0, width: radius, height: radius),
center: CGPoint.init(x: radius, y: radius),
raduis: radius,
startAngle: CGFloat.pi,
endAngle: CGFloat.pi * 3 / 2,
startPoint: CGPoint.init(x: 0, y: radius),
endPoint: CGPoint.init(x: 0, y: 0)
))
// 右上角
view.layer.addSublayer(createLayer(
frame: CGRect.init(x: radius, y: 0, width: radius, height: radius),
center: CGPoint.init(x: 0, y: radius),
raduis: radius,
startAngle: CGFloat.pi * 3 / 2,
endAngle: CGFloat.pi * 2,
startPoint: CGPoint.init(x: 0, y: 0),
endPoint: CGPoint.init(x: radius, y: 0)
))
// 右下角
view.layer.addSublayer(createLayer(
frame: CGRect.init(x: radius, y: radius, width: radius, height: radius),
center: CGPoint.init(x: 0, y: 0),
raduis: radius,
startAngle: 0,
endAngle: CGFloat.pi / 2,
startPoint: CGPoint.init(x: radius, y: 0),
endPoint: CGPoint.init(x: radius, y: radius)
))
// 左下角
view.layer.addSublayer(createLayer(
frame: CGRect.init(x: 0, y: radius, width: radius, height: radius),
center: CGPoint.init(x: radius, y: 0),
raduis: radius,
startAngle: CGFloat.pi / 2,
endAngle: CGFloat.pi,
startPoint: CGPoint.init(x: radius, y: radius),
endPoint: CGPoint.init(x: 0, y: radius)
))
}
// 这里直接调用
addCorner(100, view)
效果如下:
![](https://img.haomeiwen.com/i3096223/20e980f6045334fa.png)