根据 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 种方法都没有显示出使用它的任何差异。
有几件事要谈...
首先,您观察到
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
重要的是要注意——文档(主要)讨论的是动画性能......而不是“单一渲染”任务。
根据一些测试...
.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 图像应如下所示: