使用Draw无法获得良好的性能(_ rect:CGRect)

时间:2017-06-23 22:51:36

标签: ios swift uikit core-graphics

我有一个Timer(),要求视图每隔x秒刷新一次:

func updateTimer(_ stopwatch: Stopwatch) {
    stopwatch.counter = stopwatch.counter + 0.035


    Stopwatch.circlePercentage += 0.035
    stopwatchViewOutlet.setNeedsDisplay() 
}

-

class StopwatchView: UIView, UIGestureRecognizerDelegate {

let buttonClick = UITapGestureRecognizer()

    override func draw(_ rect: CGRect) {

    Stopwatch.drawStopwatchFor(view: self, gestureRecognizers: buttonClick)

    }
}

-

extension Stopwatch {

static func drawStopwatchFor(view: UIView, gestureRecognizers: UITapGestureRecognizer) {

    let scale: CGFloat = 0.8
    let joltButtonView = UIView(frame: CGRect(x: 0.0, y: 0.0, width: 75, height: 75))
    let imageView: UIImageView!

    // -- Need to hook this to the actual timer --
    let temporaryVariableForTimeIncrement = CGFloat(circlePercentage)

    // Exterior of sphere:
    let timerRadius = min(view.bounds.size.width, view.bounds.size.height) / 2 * scale
    let timerCenter = CGPoint(x: view.bounds.midX, y: view.bounds.midY)
    let path = UIBezierPath(arcCenter: timerCenter, radius: timerRadius, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)

    path.lineWidth = 2.0

    UIColor.blue.setStroke()
    path.stroke()

    // Interior of sphere
    let startAngle = -CGFloat.pi / 2
    let arc = CGFloat.pi * 2 * temporaryVariableForTimeIncrement / 100

    let cPath = UIBezierPath()
    cPath.move(to: timerCenter)
    cPath.addLine(to: CGPoint(x: timerCenter.x + timerRadius * cos(startAngle), y: timerCenter.y))
    cPath.addArc(withCenter: timerCenter, radius: timerRadius * CGFloat(0.99), startAngle: startAngle, endAngle: arc + startAngle, clockwise: true)
    cPath.addLine(to: CGPoint(x: timerCenter.x, y: timerCenter.y))

    let circleShape = CAShapeLayer()
    circleShape.path = cPath.cgPath
    circleShape.fillColor = UIColor.cyan.cgColor
    view.layer.addSublayer(circleShape)

    // Jolt button
    joltButtonView.center = CGPoint(x: view.bounds.midX, y: view.bounds.midY)
    joltButtonView.layer.cornerRadius = 38

    joltButtonView.backgroundColor = UIColor.white

    imageView = UIImageView(frame: joltButtonView.frame)
    imageView.contentMode = .scaleAspectFit
    imageView.image = image

    joltButtonView.addGestureRecognizer(gestureRecognizers)


    view.addSubview(joltButtonView)
    view.addSubview(imageView)


    }
}

我的第一个问题是每次都会重新绘制子视图。第二个问题是,经过几秒钟后,性能开始恶化。

Too many instances of the subview

Trying to drive the blue graphic with a Timer()

我是否应该尝试设置圆形百分比图形的动画,而不是每次调用计时器功能时重绘它?

1 个答案:

答案 0 :(得分:2)

主要观察结果是您不应在draw中添加子视图。这用于渲染单个帧,您不应该在draw内的视图层次结构中添加/删除内容。因为您大约会在0.035秒内添加子视图,所以您的视图层次结构将会爆炸,会对内存和性能产生不利影响。

您应该使用draw方法仅在更新的stroke上为秒针调用UIBezierPath。或者,如果使用CAShapeLayer,只需更新其path(根本不需要draw方法。)

所以,几个外围观察:

  1. 您正在为计时器的每个刻度增加“百分比”。但是你不能保证你的计时器被调用的频率。因此,不是增加一些“百分比”,而是应该在启动计时器时保存开始时间,然后在计时器的每个滴答时,你应该重新计算从那时到现在经过多长时间的时间。过去。

  2. 您正在使用Timer这对大多数用途都有好处,但为了获得最佳绘图效果,您真的应该使用CADisplayLink,这是最佳定时允许您执行的操作在下次刷新屏幕之前你需要的任何东西。

  3. 所以,这是一个简单的钟面的示例,其中有一个全面的秒针:

    open class StopWatchView: UIView {
    
        let faceLineWidth: CGFloat = 5                                          // LineWidth of the face
        private var startTime:  Date?         { didSet { self.updateHand() } }  // When was the stopwatch resumed
        private var oldElapsed: TimeInterval? { didSet { self.updateHand() } }  // How much time when stopwatch paused
    
        public var elapsed: TimeInterval {                                      // How much total time on stopwatch
            guard let startTime = startTime else { return oldElapsed ?? 0 }
    
            return Date().timeIntervalSince(startTime) + (oldElapsed ?? 0)
        }
    
        private weak var displayLink: CADisplayLink?                            // Display link animating second hand
        public var isRunning: Bool { return displayLink != nil }                // Is the timer running?
    
        private var clockCenter: CGPoint {                                      // Center of the clock face
            return CGPoint(x: bounds.midX, y: bounds.midY)
        }
    
        private var radius: CGFloat {                                           // Radius of the clock face
            return (min(bounds.width, bounds.height) - faceLineWidth) / 2
        }
    
        private lazy var face: CAShapeLayer = {                                 // Shape layer for clock face
            let shapeLayer = CAShapeLayer()
            shapeLayer.lineWidth   = self.faceLineWidth
            shapeLayer.strokeColor = #colorLiteral(red: 0.1215686277, green: 0.01176470611, blue: 0.4235294163, alpha: 1).cgColor
            shapeLayer.fillColor   = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 0).cgColor
            return shapeLayer
        }()
    
        private lazy var hand: CAShapeLayer = {                                 // Shape layer for second hand
            let shapeLayer = CAShapeLayer()
            shapeLayer.lineWidth = 3
            shapeLayer.strokeColor = #colorLiteral(red: 0.2196078449, green: 0.007843137719, blue: 0.8549019694, alpha: 1).cgColor
            shapeLayer.fillColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 0).cgColor
            return shapeLayer
        }()
    
        override public init(frame: CGRect = .zero) {
            super.init(frame: frame)
    
            configure()
        }
    
        required public init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
    
            configure()
        }
    
        // add necessary shape layers to layer hierarchy
    
        private func configure() {
            layer.addSublayer(face)
            layer.addSublayer(hand)
        }
    
        // if laying out subviews, make sure to resize face and update hand
    
        override open func layoutSubviews() {
            super.layoutSubviews()
    
            updateFace()
            updateHand()
        }
    
        // stop display link when view disappears
        //
        // this prevents display link from keeping strong reference to view after view is removed
    
        override open func willMove(toSuperview newSuperview: UIView?) {
            if newSuperview == nil {
                pause()
            }
        }
    
        // MARK: - DisplayLink routines
    
        /// Start display link
    
        open func resume() {
            // cancel any existing display link, if any
    
            pause()
    
            // start new display link
    
            startTime = Date()
            let displayLink = CADisplayLink(target: self, selector: #selector(handleDisplayLink(_:)))
            displayLink.add(to: .main, forMode: .commonModes)
    
            // save reference to it
    
            self.displayLink = displayLink
        }
    
        /// Stop display link
    
        open func pause() {
            displayLink?.invalidate()
    
            // calculate floating point number of seconds
    
            oldElapsed = elapsed
            startTime = nil
        }
    
        open func reset() {
            pause()
    
            oldElapsed = nil
        }
    
        /// Display link handler
        ///
        /// Will update path of second hand.
    
        @objc func handleDisplayLink(_ displayLink: CADisplayLink) {
            updateHand()
        }
    
        /// Update path of clock face
    
        private func updateFace() {
            face.path = UIBezierPath(arcCenter: clockCenter, radius: radius, startAngle: 0, endAngle: .pi * 2, clockwise: true).cgPath
        }
    
        /// Update path of second hand
    
        private func updateHand() {
            // convert seconds to an angle (in radians) and figure out end point of seconds hand on the basis of that
    
            let angle = CGFloat(elapsed / 60 * 2 * .pi - .pi / 2)
            let endPoint = CGPoint(x: clockCenter.x + cos(angle) * radius * 0.9, y: clockCenter.y + sin(angle) * radius * 0.9)
    
            // update path of hand
    
            let path = UIBezierPath()
            path.move(to: clockCenter)
            path.addLine(to: endPoint)
            hand.path = path.cgPath
        }
    }
    

    class ViewController: UIViewController {
    
        @IBOutlet weak var stopWatchView: StopWatchView!
    
        @IBAction func didTapStartStopButton () {
            if stopWatchView.isRunning {
                stopWatchView.pause()
            } else {
                stopWatchView.resume()
            }
        }
    
        @IBAction func didTapResetButton () {
            stopWatchView.reset()
        }
    
    }
    

    现在,在上面的示例中,我只是更新CAShapeLayer处理程序中秒针的CADisplayLink。就像我在开始时所说的那样,您也可以使用draw方法简单地描绘单帧动画所需的路径,然后在显示链接处理程序中调用setNeedsDisplay。但是,如果您这样做,请不要更改draw中的视图层次结构,而是在initdraw中执行您需要的任何配置stroke无论路径应该是什么就在那一刻。