离屏渲染

作者: 番茄炒西红柿啊 | 来源:发表于2020-07-08 13:26 被阅读0次

    什么是离屏渲染

    正常情况下 , 渲染完成的内容放在帧缓存区(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调试可以看到的确触发了离屏渲染 (出现了黄色的图层就表示触发了离屏渲染), 如下图 :


    1.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
    
    2.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
    
    3.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)
    
    4.png

    可见, 当存在子视图的时候, 对父视图做圆角处理, 打开masksToBounds裁剪的时候会触发离屏渲染.

    对于UIImageView

    imageView.layer.cornerRadius = 100
    imageView.layer.masksToBounds = true
    imageView.contentMode = .scaleAspectFill
    imageView.image = UIImage.init(named: "timg.jpeg")
    
    UIImageView切圆角.png
    iOS9.0 之后单纯的设置圆角, UIImageView并不会触发离屏渲染. 但是如果我们加上了边框或者背景色效果就不一样了.
    // 加上边框
    imageView.layer.borderWidth = 2.0
    imageView.layer.borderColor = UIColor.black.cgColor
    
    加上边框.png
    // 加上背景色
    imageView.backgroundColor = .cyan
    
    加上背景色.png

    可见当UIImageView存在背景色或者边框的时候进行圆角处理会触发离屏渲染.

    之所以会有上面所述的差异, 是因为当layer存在sublayer(比如: 存在子视图)或者content不为空(比如: UIImageView的image不为空)时, 同时设置了backgroundColor或者border进行裁剪时就会触发离屏渲染.

    我们可以看下layer的层级结构:


    layer

    切圆角产生离屏渲染流程图: 下图非原创, 引用出处点我跳转
    )

    那么如何尽量避免离屏渲染呢, 可行的实现方法大概有下面几种:

    此处文字引用自出处: 点我跳转

    1. 【换资源】直接使用带圆角的图片,或者替换背景色为带圆角的纯色背景图,从而避免使用圆角裁剪。不过这种方法需要依赖具体情况,并不通用。
    2. 【mask】再增加一个和背景色相同的遮罩 mask 覆盖在最上层,盖住四个角,营造出圆角的形状。但这种方式难以解决背景色为图片或渐变色的情况。
    3. 【UIBezierPath】用贝塞尔曲线绘制闭合带圆角的矩形,在上下文中设置只有内部可见,再将不带圆角的 layer 渲染成图片,添加到贝塞尔矩形中。这种方法效率更高,但是 layer 的布局一旦改变,贝塞尔曲线都需要手动地重新绘制,所以需要对 frame、color 等进行手动地监听并重绘。
    4. 【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的方案, 但是以我个人的理解, 我觉得这样设置遮罩应该还是会触发离屏渲染的. 我运行调试的时候发现, 的确会有离屏渲染, 调试出现了黄色:


    我想的方案是使用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://www.haomeiwen.com/subject/salqcktx.html