计时器间歇性漏秒

Intermittent Missing Seconds with Timer

我有一个定时器,它是一个单例,每秒重复触发。我允许用户暂停计时器以及恢复。我正在跟踪计时器的开始日期,并从经过的时间中减去任何暂停。

不幸的是,我似乎无法解决暂停和恢复计时器导致跳过一秒的间歇性问题。

例如,在下面的代码块中,我启动计时器并打印秒数:

1.0
2.0
3.0
4.0
5.0
6.0
7.0
8.0
9.0
10.0
11.0
12.0
13.0
14.0
15.0
16.0
17.0
18.0
19.0

在下面的代码块中,我恢复计时器,再次打印秒数。但是,如你所见,第20秒还没有打印出来:

21.0
22.0
23.0
24.0
25.0
26.0

我似乎无法弄清楚我在哪里失去了第二个。每次暂停和恢复周期都不会发生这种情况。

我用来跟踪上述内容的属性如下:

/// The start date of the timer.
private var startDate = Date()

/// The pause date of the timer.
private var pauseDate = Date()

/// The number of paused seconds.
private var paused = TimeInterval()

/// The number of seconds that have elapsed since the initial fire.
private var elapsed = TimeInterval()

我通过创建计时器并设置开始日期来启动计时器:

/// Starts the shower timer.
func startTimer() {
    // Fire the timer every second.
    timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(updateElapsedSeconds), userInfo: nil, repeats: true)
    // Set the start time of the initial fire.
    startDate = Date()
}

如果用户暂停计时器,则执行以下方法:

/// Pauses the shower timer.
func pauseTimer() {
    // Pause the timer.
    timer?.invalidate()
    // Set the timer to `nil`, according to the documentation.
    timer = nil
    // Set the date of the pause.
    pauseDate = Date()
}

然后,当用户恢复计时器时执行以下方法:

/// Resumes the timer.
func resumeTimer() {
    // Recreate the timer.
    timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(updateElapsedSeconds), userInfo: nil, repeats: true)
    // Add the number of paused seconds to the `paused` property.
    paused += Date().timeIntervalSince(pauseDate)
}

以下方法(由计时器触发时执行的方法调用)设置自初始触发以来经过的秒数,减去任何暂停的总和:

/// Sets the number of elapsed seconds since the timer has been started, accounting for pauses, if any.
private func updateElapsedTime() {
    // Get the date for now.
    let now = Date()
    // Get the time that has elapsed since the initial fire of the timer, and subtract any pauses.
    elapsed = now.timeIntervalSince(startDate).rounded(.down).subtracting(paused.rounded(.down))
}

最后,下面的方法是定时器触发时执行的Selector

/// Updates the number of elapsed seconds since the timer has been firing.
@objc private func updateElapsedSeconds() {
    // Configure the elapsed time with each fire.
    updateElapsedTime()
    // Post a notification when the timer fires, passing a dictionary that includes the number of elapsed seconds.
    NotificationCenter.default.post(name: CustomNotification.showerTimerFiredNotification, object: nil, userInfo: nil)
}

我做错了什么导致间歇性地漏秒?

所以这里的问题是 Timer 这种方式不准确。或者更确切地说,它的计时是相当准确的,但实际发射率有一些差异,因为它取决于运行循环。

From the documentation:

A timer is not a real-time mechanism; it fires only when one of the run loop modes to which the timer has been added is running and able to check if the timer’s firing time has passed.

为了展示这一点,我去掉了您代码中的所有舍入并打印了输出(您甚至不需要停下来就可以看到这一点)。这是这个差异的样子:

18.0004420280457
19.0005180239677
20.0004770159721
21.0005570054054
21.9997390508652
23.0003360509872
24.0003190040588
24.9993720054626
25.9991790056229

有时它触发得特别晚,这会导致整个事情都被抛弃。四舍五入没有帮助,因为你仍然依赖于实际参考时间的计时器,最终它会关闭超过一秒。

有几种方法可以解决这里的问题,具体取决于您要完成的具体目标。如果您绝对需要实际时间,您可以将计时器调整为在几分之一秒内触发,而不是使用该输出来更准确地估计秒数。这是更多的工作,仍然不会完全正确(总会有差异)。

根据您的代码,似乎只需根据计时器递增一个数字就足以实现您的目标。这是对您的代码进行的简单修改,使这项工作有效。这将简单地计数,并且无论您是否暂停,都不会在计数中跳过一秒钟:

/// The number of seconds that have elapsed since the initial fire.
private var elapsed = 0

private var timer: Timer?

/// Starts the shower timer.
func startTimer() {
    elapsed = 0
    // Fire the timer every second.
    timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(updateElapsedSeconds), userInfo: nil, repeats: true)
}

/// Pauses the shower timer.
func pauseTimer() {
    // Pause the timer.
    timer?.invalidate()
    // Set the timer to `nil`, according to the documentation.
    timer = nil
}

/// Resumes the timer.
func resumeTimer() {
    // Recreate the timer.
    timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(updateElapsedSeconds), userInfo: nil, repeats: true)
}

/// Sets the number of elapsed seconds since the timer has been started, accounting for pauses, if any.
private func updateElapsedTime() {
    // Get the time that has elapsed since the initial fire of the timer, and subtract any pauses.
    elapsed += 1
    // debug print
    print(elapsed)
}

/// Updates the number of elapsed seconds since the timer has been firing.
@objc private func updateElapsedSeconds() {
    // Configure the elapsed time with each fire.
    updateElapsedTime()
    // Post a notification when the timer fires, passing a dictionary that includes the number of elapsed seconds.
    NotificationCenter.default.post(name: CustomNotification.showerTimerFiredNotification, object: nil, userInfo: nil)
}