iOS精选面试题iOS小集

离屏渲染

2020-07-08  本文已影响0人  番茄炒西红柿啊

什么是离屏渲染

正常情况下 , 渲染完成的内容放在帧缓存区(Framebuffer) , 屏幕不断的从中Framebuffer获取内容并展示 .

离屏渲染指的是在正常情况下额外创建了离屏渲染缓存区(Offscreenbuffer) , 现将提前渲染的内容存放在了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)

效果如下:


上一篇 下一篇

猜你喜欢

热点阅读