摘要
离屏渲染是指 GPU 不在当前屏幕缓冲区进行渲染,会创建新的缓冲区。
为什么会有离屏渲染?
因为 GPU 是一层层地往画布上输出,但对于某些情况,必须完整输出之后,才能知道要对已有图层做哪些处理。
典型场景:圆角。
圆角
个人理解,对于圆角,只有一层的话,若一次遍历就可裁剪完成,就不会触发离屏渲染。
若超过 1 层,比如说添加子 view,不使用 masksToBounds 是无法裁剪好的,而设置了就会触发离屏渲染。
接下来试验一下。
先将 Simulator 的 Color Off-screen Rendered
打开(Instruments 已无法查看,只能在 Xcode 和 Simulator 中设置)
设置圆角,常见的方法,直接设置 CALayer 相关属性(cornerRadius > 0 & masksToBounds = true)
只有 1 层,设置圆角,无离屏渲染。
无论是否设置 masksToBounds,圆角都能生效。
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let wh: CGFloat = 80
let v = UIView()
v.frame = .init(x: 50, y: 100, width: wh, height: wh);
v.layer.cornerRadius = wh/2;
v.layer.masksToBounds = true;
// 单单设置背景色不会有离屏渲染,即使是非纯色背景也是如此。
v.backgroundColor = .gray;
view.addSubview(v)
}
}
超过 1 层,会有离屏渲染
需设置 masksToBounds,圆角才能生效。
// 在原代码基础上,添加一个 sv
let sv = UIView()
let p: CGFloat = 5
sv.frame = .init(x: p, y: p, width: wh-p*2, height: wh-p*2)
sv.backgroundColor = .white
v.addSubview(sv)
分析
对于超过 1 层的情况,比如说子 view 因父 view 有圆角,也需要被裁剪,只能等所有子 view 都渲染完成,再一并裁剪。
所以,GPU 就需要在另一区域,先输出完整后做裁剪,再转移到缓存区,最后展示出来。
可以用下图表示此流程:
综上,可以简单地认为:
若是圆角需设置 masksToBounds 才能生效,那么会触发离屏渲染;否则,即使设置了,也不会触发。
特别地,iOS9 之后 UIImageView 使用 png,即使 cornerRadius > 0 & masksToBounds = true 也不会触发离屏渲染。
很好奇,系统是如何做到这一点的呢?有知道的同学,麻烦告知一声,不胜感激!
解决方案
设置圆角引起离屏渲染,GPU 在 2 个缓冲区来回切换,会浪费不少时间。
那要如何减少离屏渲染呢?
- 不可避免的离屏渲染,可使用缓存,减少次数。
- 对于图片,可对图片进行裁剪,避免离屏渲染。
缓存
缓存渲染结果,虽无法避免离屏渲染,但可以减少次数。
注意渲染结果有大小限制,也会过期。
v.layer.shouldRasterize = YES; // 缓存视图渲染内容,视图不变情况下,下次绘制时可直接显示缓存
v.layer.rasterizationScale = label.layer.contentsScale;
比如说对于 4 个圆角不一致的场景,使用 layer + mask 的方法,无法避免离屏渲染,就可以进行缓存。
添加不同圆角的示例代码:
public extension UIView {
func addRoundedCorners(_ corner: UIRectCorner, raddi: CGSize, fillColor: UIColor) {
let maskLayer = self.mask(for: corner, raddi: raddi, fillColor: fillColor)
self.layer.mask = maskLayer
}
func mask(for corner: UIRectCorner, raddi: CGSize, fillColor: UIColor) -> CALayer {
let maskLayer = CAShapeLayer()
maskLayer.frame = self.bounds
let path = UIBezierPath(roundedRect: maskLayer.bounds, byRoundingCorners: corner, cornerRadii: raddi)
maskLayer.fillColor = fillColor.cgColor
maskLayer.backgroundColor = UIColor.clear.cgColor
maskLayer.path = path.cgPath
return maskLayer
}
}
图片圆角
对于图片的圆角,展示前先使用 UIGraphicsBeginImageContextWithOptions 为图片裁剪圆角。
示例代码:
@implementation UIImage (Corner)
- (UIImage *)my_imageByRoundCornerRadius:(CGFloat)radius
resize:(CGSize)resize
corners:(UIRectCorner)corners
borderWidth:(CGFloat)borderWidth
borderColor:(UIColor *)borderColor
borderLineJoin:(CGLineJoin)borderLineJoin {
if (resize.width <= 0 || resize.height <= 0 || isnan(resize.width) || isnan(resize.height) ) {
return self;
}
if (corners != UIRectCornerAllCorners) {
UIRectCorner tmp = 0;
if (corners & UIRectCornerTopLeft) tmp |= UIRectCornerBottomLeft;
if (corners & UIRectCornerTopRight) tmp |= UIRectCornerBottomRight;
if (corners & UIRectCornerBottomLeft) tmp |= UIRectCornerTopLeft;
if (corners & UIRectCornerBottomRight) tmp |= UIRectCornerTopRight;
corners = tmp;
}
UIGraphicsBeginImageContextWithOptions(resize, NO, [UIScreen mainScreen].scale);
CGContextRef context = UIGraphicsGetCurrentContext();
CGRect rect = CGRectMake(0, 0, resize.width, resize.height);
CGContextScaleCTM(context, 1, -1);
CGContextTranslateCTM(context, 0, -rect.size.height);
CGFloat minSize = MIN(resize.width, resize.height);
if (borderWidth < minSize / 2) {
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(rect, borderWidth, borderWidth) byRoundingCorners:corners cornerRadii:CGSizeMake(radius, borderWidth)];
[path closePath];
CGContextSaveGState(context);
[path addClip];
CGContextDrawImage(context, rect, self.CGImage);
CGContextRestoreGState(context);
}
if (borderColor && borderWidth < minSize / 2 && borderWidth> 0) {
CGFloat strokeInset = (floor(borderWidth * self.scale) + 0.5) / self.scale;
CGRect strokeRect = CGRectInset(rect, strokeInset, strokeInset);
CGFloat strokeRadius = radius > self.scale / 2 ? radius - self.scale / 2 : 0;
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:strokeRect byRoundingCorners:corners cornerRadii:CGSizeMake(strokeRadius, borderWidth)];
[path closePath];
path.lineWidth = borderWidth;
path.lineJoinStyle = borderLineJoin;
[borderColor setStroke];
[path stroke];
}
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
Swift 版本的示例:
func my_drawRectWithRoundedCorner(radius radius: CGFloat,
borderWidth: CGFloat,
backgroundColor: UIColor,
borderColor: UIColor) -> UIImage {
UIGraphicsBeginImageContextWithOptions(sizeToFit, false, UIScreen.mainScreen().scale)
let context = UIGraphicsGetCurrentContext()
CGContextMoveToPoint(context, 开始位置); // 开始坐标右边开始
CGContextAddArcToPoint(context, x1, y1, x2, y2, radius); // 这种类型的代码重复四次
CGContextDrawPath(UIGraphicsGetCurrentContext(), .FillStroke)
let output = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return output
}
对于 iOS10+,使用 UIGraphicsImageRenderer 代替以上做法,可避免因图片过大而引起的内存激增问题。
小结
对于圆角,只有 1 层的话,可以做到一次遍历就裁剪完成,就不会触发离屏渲染。
若超过 1 层,比如说添加子 view,不使用 masksToBounds 也无法裁剪好的,而设置了就会触发离屏渲染。
所以,可简单认为,若是圆角需设置 masksToBounds 才能生效,那么会触发离屏渲染;否则,即使设置了,也不会触发。
那要如何减少离屏渲染呢?
- 不可避免的离屏渲染,可使用缓存,减少次数。
- 对于图片,可对预先图片进行裁剪,避免离屏渲染。