Swift iOS 中与 drawRect 有关的性能问题
Performance issues pertaining to drawRect in iOS on Swift
感谢您抽出宝贵时间阅读我的帖子。最近,我一直在开发一个简单的 iOS 免费绘图应用程序,并且 运行 遇到了一个目前超出我的技能水平无法解决的问题。几天来我一直在网上搜索以尝试找到解决方案,但到目前为止还没有成功。幸运的是,我已经想到了解决我的应用程序延迟问题的方法,但是我仍然需要帮助,因为我真的不知道如何实现它。
简单说明这部分程序是如何运行的:
当用户在屏幕上移动 his/her 手指时(在标记为 UI 的视图中:view_Draw),touchesBegan() 和 touchesMoved() 解释 x&y 的起点和终点坐标并将这些坐标存储在数组(行)中。然后通过 setNeedsDisplay() 强制更新 drawView。
class view_Draw: UIView {
// view_Draw Class Variables
var lastPoint: CGPoint!
var drawColor:UIColor = UIColor.redColor()
required init(coder aDecoder:NSCoder) {
super.init(coder: aDecoder)
self.backgroundColor = UIColor.whiteColor()
}
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
lastPoint = touches.anyObject()?.locationInView(self)
}
override func touchesMoved(touches: NSSet, withEvent event: UIEvent) {
var newPoint = touches.anyObject()?.locationInView(self)
lines.append(Line(start: self.lastPoint, end: newPoint!, color: self.drawColor))
self.lastPoint = newPoint
setNeedsDisplay()
}
override func drawRect(rect: CGRect) {
var context = UIGraphicsGetCurrentContext()
//set stroke style
CGContextSetLineCap(context, kCGLineCapRound)
//set brush parameters
CGContextSetStrokeColorWithColor(context, drawColor.CGColor)
CGContextSetAlpha(context, 0.95)
CGContextSetLineWidth(context, brushSize)
for line in lines {
CGContextBeginPath(context)
CGContextMoveToPoint(context, line.start.x, line.start.y)
CGContextAddLineToPoint(context, line.end.x, line.end.y)
CGContextStrokePath(context)
// CGBitmapContextCreateImage(context)
// CGBitmapContextReleaseDataCallback()
}
}
}
问题简述:
当用户继续在屏幕上绘图时,我在仪表板中注意到线程 1 上的 CPU 达到了大约 95% – 100%。这会导致我程序中的元素(计时器、绘图响应)开始滞后。
采取的补救措施:
我通过禁用 setNeedsDisplay() 进行了试验,发现填充行数组只相当于总体 CPU 需求的 10%。据我了解,这是因为 drawRect 中的任何内容都没有应用于 lines 数组中的坐标。
禁用 CGContextStrokePath() 并启用 setNeedsDisplay() 会将 CPU 需求增加到 49%。我将此解释为 lines 数组中的坐标现在由 drawRect 操作 - 但实际上并未绘制到视图上。
这意味着通过强制 setNeedsDisplay() 在启用 CGContextStrokePath 的情况下进行更新,它占用了线程 1 大约 85% - 90% 的可用处理能力。
我也尝试过添加一个计时器来控制 setNeedsDisplay 强制更新的频率,但结果不尽如人意。有了这个,绘图感觉很不稳定。
建议补救措施:
我认为主要问题是 setNeedsDisplay() 正在重新绘制整个线条数组 - 用户绘制的内容,在访问 touchesMoved() 时不断。
我研究过可能使用 GCD 来尝试减轻线程 1 的一些负载,但是在阅读它之后,似乎这不是 'safe' 方式。据我了解,GCD and/or dispatch_async... 不应用于直接与 UI 元素交互的元素。
我在各种论坛上看到人们通过将现有路径上下文转换为位图并仅使用 setNeedsDisplay 更新新生成的路径来解决类似问题。
我希望通过这种方式解决问题,setNeedsDisplay 不必每次都实时绘制整个数组,因为之前绘制的线条将被转换为静态图像。我 运行 不知道如何开始实施它。
如您所知,我几周前才开始学习 Swift。我正在尽最大努力以有效的方式学习和解决问题。如果您对我应该如何处理此问题有任何建议,将不胜感激。再次感谢您的帮助。
答案基于Aky's Smooth Freehand Drawing on iOS tutorial
通过实施以下代码,创建了一种 "buffer" 类型,有助于减轻线程 1 上的负载。在我的初始测试中,与我的原始程序相比,绘制时负载最高为 46%最高负载为 95%-100%
LinearInterpView.swift
import UIKit
class LinearInterpView:UIView {
var path = UIBezierPath() //(3)
var bezierPath = UIBezierPath()
required init(coder aDecoder:NSCoder) { //(1)
super.init(coder: aDecoder)
self.multipleTouchEnabled = false //(2)
self.backgroundColor = UIColor.whiteColor()
path = bezierPath
path.lineWidth = 40.0
}
override func drawRect(rect: CGRect) { //(5)
UIColor.blackColor().setStroke()
path.stroke()
}
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
var touch:UITouch = touches.anyObject() as UITouch
var p:CGPoint = touch.locationInView(self)
path.moveToPoint(p)
}
override func touchesMoved(touches: NSSet, withEvent event: UIEvent) {
var touch:UITouch = touches.anyObject() as UITouch
var p:CGPoint = touch.locationInView(self)
path.addLineToPoint(p) //(4)
setNeedsDisplay()
}
override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {
touchesMoved(touches, withEvent: event)
}
override func touchesCancelled(touches: NSSet!, withEvent event: UIEvent!) {
touchesEnded(touches, withEvent: event)
}
}
CachedLIView.swift
import UIKit
class CachedLIView:UIView {
var path = UIBezierPath()
var bezierPath = UIBezierPath() // needed to add this
var incrementalImage = UIImage() //(1)
var firstRun:Bool = true
required init(coder aDecoder:NSCoder) {
super.init(coder: aDecoder)
self.multipleTouchEnabled = false
self.backgroundColor = UIColor.whiteColor()
path = bezierPath
path.lineWidth = 40.0
}
override func drawRect(rect: CGRect) {
incrementalImage.drawInRect(rect) //(3)
path.stroke()
}
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
var touch:UITouch = touches.anyObject() as UITouch
var p:CGPoint = touch.locationInView(self)
path.moveToPoint(p)
}
override func touchesMoved(touches: NSSet, withEvent event: UIEvent) {
var touch:UITouch = touches.anyObject() as UITouch
var p:CGPoint = touch.locationInView(self)
path.addLineToPoint(p)
self.setNeedsDisplay()
}
override func touchesEnded(touches: NSSet, withEvent event: UIEvent) { //(2)
var touch:UITouch = touches.anyObject() as UITouch
var p:CGPoint = touch.locationInView(self)
path.addLineToPoint(p)
self.drawBitmap() //(3)
self.setNeedsDisplay()
path.removeAllPoints() //(4)
}
override func touchesCancelled(touches: NSSet!, withEvent event: UIEvent!) {
touchesEnded(touches, withEvent: event)
}
func drawBitmap() { //(3)
var rectPath = UIBezierPath()
UIGraphicsBeginImageContextWithOptions(self.bounds.size, true, 0.0)
UIColor.blackColor().setStroke()
if(firstRun == true) {
rectPath = UIBezierPath(rect: self.bounds)
UIColor.whiteColor().setFill()
rectPath.fill()
firstRun = false
}
incrementalImage.drawAtPoint(CGPointZero)
path.stroke()
incrementalImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
}
再次感谢您的帮助,希望对其他人也有用。
几年前我写了一些关于使用 Objective-C 在 iOS 中绘图的教程。您可以将此代码翻译成 Swift 而不会出现太多问题。
第一篇文章讨论了如何在每次用户从屏幕上抬起手指时将屏幕上的图形上下文渲染成图像,这样您就可以从那个点开始生成一条新路径,然后直接绘制它在那张图片之上。
Smooth Freehand Drawing on iOS
第二个具有基于 GCD 的实现,可让您在后台执行此渲染。
感谢您抽出宝贵时间阅读我的帖子。最近,我一直在开发一个简单的 iOS 免费绘图应用程序,并且 运行 遇到了一个目前超出我的技能水平无法解决的问题。几天来我一直在网上搜索以尝试找到解决方案,但到目前为止还没有成功。幸运的是,我已经想到了解决我的应用程序延迟问题的方法,但是我仍然需要帮助,因为我真的不知道如何实现它。
简单说明这部分程序是如何运行的:
当用户在屏幕上移动 his/her 手指时(在标记为 UI 的视图中:view_Draw),touchesBegan() 和 touchesMoved() 解释 x&y 的起点和终点坐标并将这些坐标存储在数组(行)中。然后通过 setNeedsDisplay() 强制更新 drawView。
class view_Draw: UIView {
// view_Draw Class Variables
var lastPoint: CGPoint!
var drawColor:UIColor = UIColor.redColor()
required init(coder aDecoder:NSCoder) {
super.init(coder: aDecoder)
self.backgroundColor = UIColor.whiteColor()
}
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
lastPoint = touches.anyObject()?.locationInView(self)
}
override func touchesMoved(touches: NSSet, withEvent event: UIEvent) {
var newPoint = touches.anyObject()?.locationInView(self)
lines.append(Line(start: self.lastPoint, end: newPoint!, color: self.drawColor))
self.lastPoint = newPoint
setNeedsDisplay()
}
override func drawRect(rect: CGRect) {
var context = UIGraphicsGetCurrentContext()
//set stroke style
CGContextSetLineCap(context, kCGLineCapRound)
//set brush parameters
CGContextSetStrokeColorWithColor(context, drawColor.CGColor)
CGContextSetAlpha(context, 0.95)
CGContextSetLineWidth(context, brushSize)
for line in lines {
CGContextBeginPath(context)
CGContextMoveToPoint(context, line.start.x, line.start.y)
CGContextAddLineToPoint(context, line.end.x, line.end.y)
CGContextStrokePath(context)
// CGBitmapContextCreateImage(context)
// CGBitmapContextReleaseDataCallback()
}
}
}
问题简述:
当用户继续在屏幕上绘图时,我在仪表板中注意到线程 1 上的 CPU 达到了大约 95% – 100%。这会导致我程序中的元素(计时器、绘图响应)开始滞后。
采取的补救措施:
我通过禁用 setNeedsDisplay() 进行了试验,发现填充行数组只相当于总体 CPU 需求的 10%。据我了解,这是因为 drawRect 中的任何内容都没有应用于 lines 数组中的坐标。
禁用 CGContextStrokePath() 并启用 setNeedsDisplay() 会将 CPU 需求增加到 49%。我将此解释为 lines 数组中的坐标现在由 drawRect 操作 - 但实际上并未绘制到视图上。
这意味着通过强制 setNeedsDisplay() 在启用 CGContextStrokePath 的情况下进行更新,它占用了线程 1 大约 85% - 90% 的可用处理能力。
我也尝试过添加一个计时器来控制 setNeedsDisplay 强制更新的频率,但结果不尽如人意。有了这个,绘图感觉很不稳定。
建议补救措施:
我认为主要问题是 setNeedsDisplay() 正在重新绘制整个线条数组 - 用户绘制的内容,在访问 touchesMoved() 时不断。
我研究过可能使用 GCD 来尝试减轻线程 1 的一些负载,但是在阅读它之后,似乎这不是 'safe' 方式。据我了解,GCD and/or dispatch_async... 不应用于直接与 UI 元素交互的元素。
我在各种论坛上看到人们通过将现有路径上下文转换为位图并仅使用 setNeedsDisplay 更新新生成的路径来解决类似问题。
我希望通过这种方式解决问题,setNeedsDisplay 不必每次都实时绘制整个数组,因为之前绘制的线条将被转换为静态图像。我 运行 不知道如何开始实施它。
如您所知,我几周前才开始学习 Swift。我正在尽最大努力以有效的方式学习和解决问题。如果您对我应该如何处理此问题有任何建议,将不胜感激。再次感谢您的帮助。
答案基于Aky's Smooth Freehand Drawing on iOS tutorial
通过实施以下代码,创建了一种 "buffer" 类型,有助于减轻线程 1 上的负载。在我的初始测试中,与我的原始程序相比,绘制时负载最高为 46%最高负载为 95%-100%
LinearInterpView.swift
import UIKit
class LinearInterpView:UIView {
var path = UIBezierPath() //(3)
var bezierPath = UIBezierPath()
required init(coder aDecoder:NSCoder) { //(1)
super.init(coder: aDecoder)
self.multipleTouchEnabled = false //(2)
self.backgroundColor = UIColor.whiteColor()
path = bezierPath
path.lineWidth = 40.0
}
override func drawRect(rect: CGRect) { //(5)
UIColor.blackColor().setStroke()
path.stroke()
}
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
var touch:UITouch = touches.anyObject() as UITouch
var p:CGPoint = touch.locationInView(self)
path.moveToPoint(p)
}
override func touchesMoved(touches: NSSet, withEvent event: UIEvent) {
var touch:UITouch = touches.anyObject() as UITouch
var p:CGPoint = touch.locationInView(self)
path.addLineToPoint(p) //(4)
setNeedsDisplay()
}
override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {
touchesMoved(touches, withEvent: event)
}
override func touchesCancelled(touches: NSSet!, withEvent event: UIEvent!) {
touchesEnded(touches, withEvent: event)
}
}
CachedLIView.swift
import UIKit
class CachedLIView:UIView {
var path = UIBezierPath()
var bezierPath = UIBezierPath() // needed to add this
var incrementalImage = UIImage() //(1)
var firstRun:Bool = true
required init(coder aDecoder:NSCoder) {
super.init(coder: aDecoder)
self.multipleTouchEnabled = false
self.backgroundColor = UIColor.whiteColor()
path = bezierPath
path.lineWidth = 40.0
}
override func drawRect(rect: CGRect) {
incrementalImage.drawInRect(rect) //(3)
path.stroke()
}
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
var touch:UITouch = touches.anyObject() as UITouch
var p:CGPoint = touch.locationInView(self)
path.moveToPoint(p)
}
override func touchesMoved(touches: NSSet, withEvent event: UIEvent) {
var touch:UITouch = touches.anyObject() as UITouch
var p:CGPoint = touch.locationInView(self)
path.addLineToPoint(p)
self.setNeedsDisplay()
}
override func touchesEnded(touches: NSSet, withEvent event: UIEvent) { //(2)
var touch:UITouch = touches.anyObject() as UITouch
var p:CGPoint = touch.locationInView(self)
path.addLineToPoint(p)
self.drawBitmap() //(3)
self.setNeedsDisplay()
path.removeAllPoints() //(4)
}
override func touchesCancelled(touches: NSSet!, withEvent event: UIEvent!) {
touchesEnded(touches, withEvent: event)
}
func drawBitmap() { //(3)
var rectPath = UIBezierPath()
UIGraphicsBeginImageContextWithOptions(self.bounds.size, true, 0.0)
UIColor.blackColor().setStroke()
if(firstRun == true) {
rectPath = UIBezierPath(rect: self.bounds)
UIColor.whiteColor().setFill()
rectPath.fill()
firstRun = false
}
incrementalImage.drawAtPoint(CGPointZero)
path.stroke()
incrementalImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
}
再次感谢您的帮助,希望对其他人也有用。
几年前我写了一些关于使用 Objective-C 在 iOS 中绘图的教程。您可以将此代码翻译成 Swift 而不会出现太多问题。
第一篇文章讨论了如何在每次用户从屏幕上抬起手指时将屏幕上的图形上下文渲染成图像,这样您就可以从那个点开始生成一条新路径,然后直接绘制它在那张图片之上。
Smooth Freehand Drawing on iOS
第二个具有基于 GCD 的实现,可让您在后台执行此渲染。