在 GPU 上渲染 CALayer

问题描述 投票:0回答:1

根据 ldoogy 的 answer,将

drawsAsynchronously
CALayer
属性设置为 true 可以使基于 Metal 的渲染器大大提高性能。

这个网页证实了 Idoogy 的主张。

但是,当

drawsAsynchronously
设置为 true 或 false 时,我没有看到任何性能差异。

let layer = CALayer()
let drawsAsynchronously = true //Makes no difference set to true or false
shapeLayer.drawsAsynchronously = drawsAsynchronously

let f = CGRect(x: 0.0, y: 0.0, width: 1024.0, height: 1024.0)
let cgColor = UIColor.orange.cgColor

var lines: [CGPath] // populated with several hundred paths, some with hundreds of points

let start = CFAbsoluteTimeGetCurrent()
    
for path in lines {
    let pathLayer = CAShapeLayer()
    pathLayer.path = path
    pathLayer.strokeColor = cgColor
    pathLayer.fillColor = nil
    pathLayer.lineWidth = 1.0
    pathLayer.drawsAsynchronously = drawsAsynchronously
    layer.addSublayer(pathLayer)
}

UIGraphicsBeginImageContext(f.size)
let ctx = UIGraphicsGetCurrentContext()
layer.render(in: ctx!)
let newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext()

//time: 0.05170309543609619
print("time: \(CFAbsoluteTimeGetCurrent() - start)")

Idoogy 指定了

renderInContext
,这就是我上面使用的。
render(in:)
已用现代 Swift 取代了
renderInContext

令人惊讶的是,

UIGraphicsImageRenderer
慢得多(接近 300%),但如果
drawsAsynchronously
设置为 true 或 false,也没有区别:

let renderer = UIGraphicsImageRenderer(size: f.size)
let capturedImage = renderer.image { (ctx) in
    return layer.render(in: ctx.cgContext)
}

// time: 0.13654804229736328    
print("time: \(CFAbsoluteTimeGetCurrent() - start)")

我是否错过了启用硬件加速渲染并启用

drawsAsynchronously
的功能?

编辑:

我也尝试使用drawRect方法,因为Idoogy提到了

ContextStrokePath
,但它是最慢的,并且无论是否启用
drawsAsynchronously
都没有区别。

class LineView: UIView {
    var lines: [CGPath] //populated with several hundred paths, some with hundreds of points

    override func draw(_ rect: CGRect) {
        let color = UIColor.orange.cgColor
        if let context = UIGraphicsGetCurrentContext() {
            for path in lines {
                context.saveGState()
                context.addPath(path)
                context.setStrokeColor(color)
                context.setLineWidth(1.0)
                context.strokePath()
                context.restoreGState()
            }
        }
    }
}

class ViewController: UIViewController {
    override func viewDidLoad() {
            super.viewDidLoad()
            let drawsAsynchronously = true //Makes no difference set to true or false
            view.layer.drawsAsynchronously = drawsAsynchronously
            let f = CGRect(x: 0.0, y: 0.0, width: 1024.0, height: 1024.0)
            let lineView = LineView(frame: f)
            lineView.layer.drawsAsynchronously = drawsAsynchronously
            view.addSubview(lineView)
            let start = CFAbsoluteTimeGetCurrent()
            UIGraphicsBeginImageContext(f.size)
            let ctx = UIGraphicsGetCurrentContext()
            lineView.layer.render(in: ctx!)
            let newImage = UIGraphicsGetImageFromCurrentImageContext();
            UIGraphicsEndImageContext()
            //time: 0.14092397689819336
            print("time: \(CFAbsoluteTimeGetCurrent() - start)")
         }

由于它比使用

CAShapeLayer
慢得多(慢 3 倍),我怀疑使用
CAShapeLayer
可能是使用 GPU 渲染图像。我仍然想让 Idoogy 描述的方法起作用,因为我尝试过的 3 种方法都没有显示出使用它的任何差异。

ios swift cgcontext cashapelayer
1个回答
0
投票

有几件事要谈...

首先,您观察到

UIGraphicsGetImageFromCurrentImageContext
UIGraphicsImageRenderer
之间的速度差异是由于您渲染不同尺寸的图像所致。

假设您使用的是

@3x
设备,
UIGraphicsImageRenderer
正在渲染 1024 x 1024
UIImage
,但它的
.scale
3,所以它实际上是 3072 x 3072 pixel图像。

要获得等效图像,请将该代码块更改为:

    let fmt = UIGraphicsImageRendererFormat()
    fmt.scale = 1
    let renderer = UIGraphicsImageRenderer(size: f.size, format: fmt)
    let capturedImage = renderer.image { (ctx) in
        return layer.render(in: ctx.cgContext)
    }

现在

UIGraphicsImageRenderer
将生成与 UIGraphicsGetImageFromCurrentImageContext 相同的
1024 x 1024
像素图像。


接下来,您要对与

layer.drawsAsynchronously
不直接相关的代码块进行计时——创建和添加子层、生成对象等。

Apple 关于此的文档并不是我所说的“深入”——但是提高动画性能我们发现:

根据需要使用异步图层渲染

您在委托的

drawLayer:inContext:
方法或视图的
drawRect:
方法中执行的任何绘图通常都会在应用程序的主线程上同步发生。但在某些情况下,同步绘制内容可能无法提供最佳性能。如果您发现动画效果不佳,您可以尝试启用图层上的
drawsAsynchronously
属性,将这些操作移至后台线程。如果这样做,请确保您的绘图代码是线程安全的。与往常一样,在将异步绘图放入生产代码之前,您应该始终测量异步绘图的性能。

重要的是要注意——文档(主要)讨论的是动画性能......而不是“单一渲染”任务。


根据一些测试...

  • 看来图层必须位于视图层次结构中。它实际上不必在屏幕上“可见”,但它不能是“独立”层。 许多(例如 10,000 个)
  • 简单路径
  • 将需要 更长才能使用 .drawsAsynchronously = true
    进行渲染
    
    很少(例如 20 个)
  • 非常复杂的路径
  • 将需要 更长才能使用 .drawsAsynchronously = false
    进行渲染
    
    与几乎所有事情一样,我们“第一次”使用子系统会产生开销......因此,当尝试执行诸如测量渲染持续时间之类的操作时,我们希望在开始计时之前运行它几次。
  • 那么,让我们看一些实际的示例代码来演示其中的差异。这里要尝试和详细说明的内容太多,但内嵌注释应该可以清楚地说明发生了什么:

自定义

CALayer
子类

class MyCustomDrawLayer: CALayer {
    
    // a property so the caller can read the last draw() duration
    var lastRenderDuration: Double = -1
    
    var pths: [CGPath] = []
    
    var cgStrokeColors: [CGColor] = []
    var cgFillColors: [CGColor] = []
    
    override init() {
        super.init()
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() {
        let lineColors: [UIColor] = [
            .red, .systemGreen, .blue
        ]
        let fillColors: [UIColor] = [
            .yellow, .green, .cyan
        ]
        cgStrokeColors = lineColors.map({$0.cgColor})
        cgFillColors = fillColors.map({$0.cgColor})
    }
    
    override func draw(in ctx: CGContext) {
        super.draw(in: ctx)
        guard pths.count > 0 else { return }
        
        let drawStart = CFAbsoluteTimeGetCurrent()
        
        // cycle through 3 stroke/fill color sets as we draw the paths
        for (i, pth) in pths.enumerated() {
            ctx.setStrokeColor(cgStrokeColors[i % cgStrokeColors.count])
            ctx.setFillColor(cgFillColors[i % cgFillColors.count])
            ctx.addPath(pth)
            ctx.drawPath(using: .fillStroke)
        }
        
        let drawEnd = CFAbsoluteTimeGetCurrent()
        lastRenderDuration = drawEnd - drawStart
    }
    
}

“迅捷鸟”之路

class SwiftyBird: NSObject { func path(inRect: CGRect) -> UIBezierPath { let thisShape = UIBezierPath() thisShape.move(to: CGPoint(x: 0.31, y: 0.94)) thisShape.addCurve(to: CGPoint(x: 0.00, y: 0.64), controlPoint1: CGPoint(x: 0.18, y: 0.87), controlPoint2: CGPoint(x: 0.07, y: 0.76)) thisShape.addCurve(to: CGPoint(x: 0.12, y: 0.72), controlPoint1: CGPoint(x: 0.03, y: 0.67), controlPoint2: CGPoint(x: 0.07, y: 0.70)) thisShape.addCurve(to: CGPoint(x: 0.57, y: 0.72), controlPoint1: CGPoint(x: 0.28, y: 0.81), controlPoint2: CGPoint(x: 0.45, y: 0.80)) thisShape.addCurve(to: CGPoint(x: 0.57, y: 0.72), controlPoint1: CGPoint(x: 0.57, y: 0.72), controlPoint2: CGPoint(x: 0.57, y: 0.72)) thisShape.addCurve(to: CGPoint(x: 0.15, y: 0.23), controlPoint1: CGPoint(x: 0.40, y: 0.57), controlPoint2: CGPoint(x: 0.26, y: 0.39)) thisShape.addCurve(to: CGPoint(x: 0.10, y: 0.15), controlPoint1: CGPoint(x: 0.13, y: 0.21), controlPoint2: CGPoint(x: 0.11, y: 0.18)) thisShape.addCurve(to: CGPoint(x: 0.50, y: 0.49), controlPoint1: CGPoint(x: 0.22, y: 0.28), controlPoint2: CGPoint(x: 0.43, y: 0.44)) thisShape.addCurve(to: CGPoint(x: 0.22, y: 0.09), controlPoint1: CGPoint(x: 0.35, y: 0.31), controlPoint2: CGPoint(x: 0.21, y: 0.08)) thisShape.addCurve(to: CGPoint(x: 0.69, y: 0.52), controlPoint1: CGPoint(x: 0.46, y: 0.37), controlPoint2: CGPoint(x: 0.69, y: 0.52)) thisShape.addCurve(to: CGPoint(x: 0.71, y: 0.54), controlPoint1: CGPoint(x: 0.70, y: 0.53), controlPoint2: CGPoint(x: 0.70, y: 0.53)) thisShape.addCurve(to: CGPoint(x: 0.61, y: 0.00), controlPoint1: CGPoint(x: 0.77, y: 0.35), controlPoint2: CGPoint(x: 0.71, y: 0.15)) thisShape.addCurve(to: CGPoint(x: 0.92, y: 0.68), controlPoint1: CGPoint(x: 0.84, y: 0.15), controlPoint2: CGPoint(x: 0.98, y: 0.44)) thisShape.addCurve(to: CGPoint(x: 0.92, y: 0.70), controlPoint1: CGPoint(x: 0.92, y: 0.69), controlPoint2: CGPoint(x: 0.92, y: 0.70)) thisShape.addCurve(to: CGPoint(x: 0.92, y: 0.70), controlPoint1: CGPoint(x: 0.92, y: 0.70), controlPoint2: CGPoint(x: 0.92, y: 0.70)) thisShape.addCurve(to: CGPoint(x: 0.99, y: 1.00), controlPoint1: CGPoint(x: 1.00, y: 0.86), controlPoint2: CGPoint(x: 1.00, y: 1.00)) thisShape.addCurve(to: CGPoint(x: 0.75, y: 0.93), controlPoint1: CGPoint(x: 0.92, y: 0.86), controlPoint2: CGPoint(x: 0.81, y: 0.90)) thisShape.addCurve(to: CGPoint(x: 0.31, y: 0.94), controlPoint1: CGPoint(x: 0.64, y: 1.01), controlPoint2: CGPoint(x: 0.47, y: 1.00)) thisShape.close() let tr = CGAffineTransform(translationX: inRect.minX, y: inRect.minY) .scaledBy(x: inRect.width, y: inRect.height) thisShape.apply(tr) return thisShape } } 如果

inRect
 是(大约)200x200,则看起来像这样:

测试视图控制器类

:

class MyDrawAsyncTestVC: UIViewController { let customLayer = MyCustomDrawLayer() var manySimplePaths: [CGPath] = [] var fewComplexPaths: [CGMutablePath] = [] var bUseManyPaths: Bool = true override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .systemYellow // generate a 64x64 "grid" of 16x16 paths // (fills the 1024x1024 size) let v: CGFloat = 16.0 var r: CGRect = .init(x: 0.0, y: 0.0, width: v, height: v) for col in 0..<64 { for row in 0..<64 { r.origin = .init(x: CGFloat(col) * v, y: CGFloat(row) * v) manySimplePaths.append(SwiftyBird().path(inRect: r).cgPath) } } // manySimplePaths has 4096 paths fewComplexPaths = [ CGMutablePath(), CGMutablePath(), CGMutablePath(), ] for (j, pth) in manySimplePaths.enumerated() { fewComplexPaths[j % fewComplexPaths.count].addPath(pth) } // fewComplexPaths produces the same output, // but uses only 3 paths: // [0] has 1366 subpaths // [1] has 1365 subpaths // [2] has 1365 subpaths customLayer.pths = manySimplePaths // the custom layer MUST be in the view hierarchy, // but it doesn't have to be visible // so we'll add it as a sublayer but position it "out-of-frame" let sz: CGSize = .init(width: 1024.0, height: 1024.0) customLayer.frame = .init(x: -(sz.width + 10.0), y: -(sz.height + 10.0), width: sz.width, height: sz.height) view.layer.addSublayer(customLayer) } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) // let's call the test/render func every second Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { _ in self.testMe() }) } // tap anywhere to toggle between manySimplePaths and fewComplexPaths override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { bUseManyPaths.toggle() customLayer.pths = bUseManyPaths ? manySimplePaths : fewComplexPaths print("\nSwitched to:", bUseManyPaths ? "manySimplePaths" : "fewComplexPaths", "\n") } var iCount: Int = 0 func testMe() { let f = customLayer.frame // toggle .drawsAsynchronously each time through customLayer.drawsAsynchronously.toggle() let genImageStart = CFAbsoluteTimeGetCurrent() UIGraphicsBeginImageContext(f.size) guard let ctx = UIGraphicsGetCurrentContext() else { fatalError("Could not get Context!!!") } let renderStart = CFAbsoluteTimeGetCurrent() // we want .render(in:) to trigger a call to draw(in:) in custom layer customLayer.setNeedsDisplay() // render the layer customLayer.render(in: ctx) let renderEnd = CFAbsoluteTimeGetCurrent() let newImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() let genImageEnd = CFAbsoluteTimeGetCurrent() if iCount == 0 { print("\nWe're ignoring the first few timing values, so we're not measuring overhead...") } iCount += 1 if iCount < 4 { print(iCount) return } var s: String = "async: \(customLayer.drawsAsynchronously)" s += customLayer.drawsAsynchronously ? "\t\t" : "\t" s += "Draw Time: " s += String(format: "%0.10f", customLayer.lastRenderDuration) s += "\t\t" s += "Render Time: " s += String(format: "%0.10f", renderEnd - renderStart) s += "\t\t" s += "Gen Image Time: " s += String(format: "%0.10f", genImageEnd - genImageStart) print(s) } } 运行时,我们在屏幕上看不到任何内容(只有黄色背景,因此我们知道应用程序处于“活动”状态)。

它启动一个计时器,每秒渲染一张 1024x1024 图像,在 
.drawsAsynchronously

true/false 之间交替,并将计时统计信息打印到调试控制台。

点击任意位置可在渲染

manySimplePaths

fewComplexPaths

之间切换——两者都会产生完全相同的输出图像。

您应该在调试控制台中看到类似的内容:
2024-01-17 13:00:38.706243-0500 MyProj[66254:6949370] Metal GPU Frame Capture Enabled
2024-01-17 13:00:38.708287-0500 MyProj[66254:6949370] Metal API Validation Enabled

We're ignoring the first few timing values, so we're not measuring overhead...
1
2
3
async: false    Draw Time: 0.0868519545     Render Time: 0.0882049799       Gen Image Time: 0.0901809931
async: true     Draw Time: 0.0249859095     Render Time: 0.1147090197       Gen Image Time: 0.1166020632
async: false    Draw Time: 0.0890671015     Render Time: 0.0899358988       Gen Image Time: 0.0919650793
async: true     Draw Time: 0.0232139826     Render Time: 0.1093589067       Gen Image Time: 0.1112560034

Switched to: fewComplexPaths 

async: false    Draw Time: 0.1343829632     Render Time: 0.1352089643       Gen Image Time: 0.1371099949
async: true     Draw Time: 0.0092250109     Render Time: 0.0681159496       Gen Image Time: 0.0701240301
async: false    Draw Time: 0.1334309578     Render Time: 0.1342890263       Gen Image Time: 0.1361669302
async: true     Draw Time: 0.0110900402     Render Time: 0.0679899454       Gen Image Time: 0.0699119568

渲染的 1024x1024 图像应如下所示:

© www.soinside.com 2019 - 2024. All rights reserved.