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

Can't get good performance with Draw(_ rect: CGRect)

我有一个 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()

我是否应该尝试为圆形百分比图形设置动画,而不是在每次调用计时器函数时都重新绘制它?

主要观察是您不应该在 draw 中添加子视图。这是为了渲染单个帧,你不应该是 draw 内视图层次结构中的 adding/removing 东西。因为您大约每 0.035 秒添加一次子视图,您的视图层次结构将会爆炸,对内存和性能产生不利影响。

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

所以,一些外围观察:

  1. 您正在为计时器的每个滴答递增 "percentage"。但是您不能保证调用计时器的频率。因此,与其增加一些 "percentage",您应该在启动计时器时保存开始时间,然后在计时器的每个滴答声中,您应该重新计算从那时到现在之间经过的时间量,以确定有多少时间过去了。

  2. 您使用的 Timer 适合大多数用途,但为了获得最佳绘图效果,您确实应该使用 CADisplayLink,它的最佳时机是允许您在下次刷新屏幕之前做任何您需要做的事情。

所以,这里有一个带有扫秒针的简单钟面示例:

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()
    }

}

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