【问题标题】:How to prevent Timer slowing down in background如何防止定时器在后台变慢
【发布时间】:2020-06-13 13:10:11
【问题描述】:

我正在用 Swift 编写一个 Mac OS 应用程序,并希望每 0.5 秒重复一次任务(或多或少,不需要高精度)。当我使用其他应用程序时,此应用程序应该在后台运行。

我目前正在使用Timer

Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true)

它开始正常,大约每 0.5 秒更新一次,但在后台运行一段时间后,计时器会大幅减慢到大约 1 秒或 2 秒的间隔(它非常接近这些值,似乎计时器跳过滴答声或有一个2 或 4 倍的减速)。

我怀疑这是因为应用程序在后台几秒钟后被赋予了低优先级。有没有办法避免这种情况?它可以在 XCode 的应用程序设置中通过要求始终保持活动状态,也可以在应用程序运行时从系统中获取(或者甚至在没有 Timer 的情况下以不同的方式做事,但如果可能的话,我宁愿保持简单)。

这是一个最小的工作示例:应用程序只有一个带有此代码的 ViewController

import Cocoa


class ViewController: NSViewController {
    var lastUpdate = Date().timeIntervalSince1970

    override func viewDidLoad() {
        super.viewDidLoad()

        let timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) {
            timer in
            let now = Date().timeIntervalSince1970
            print(now - self.lastUpdate)
            self.lastUpdate = now
        }
        RunLoop.current.add(timer, forMode: .common)
    }
}

开始时的输出是

0.5277011394500732
0.5008649826049805
0.5000109672546387
0.49898695945739746
0.5005381107330322
0.5005340576171875
0.5000457763671875
...

但在后台几秒钟后它变成了

0.49993896484375
0.49997520446777344
0.5000619888305664
1.5194149017333984
1.0009620189666748
0.9984869956970215
2.0002501010894775
2.001321792602539
1.9989290237426758
...

如果我将应用程序带回前台,计时器将返回 0.5 秒增量。

注意:我在 iMac 上运行 Mac OSX 10.15.5 (Catalina)

【问题讨论】:

    标签: swift macos


    【解决方案1】:

    这是因为App Nap。您可以禁用 App Nap,但不建议这样做。

    var activity: NSObjectProtocol?
    activity = ProcessInfo().beginActivity(options: .userInitiatedAllowingIdleSystemSleep, reason: "Timer delay")
    

    timer 的默认tolerance 值为零,但系统保留对某些定时器应用少量容差的权利,无论容差属性的值如何。

    【讨论】:

    • 感谢您的回答。正如您所建议的,AppNap 可能是问题的原因。但是,添加beginActivity 行并不能解决问题(即使使用.latencyCritical)。约 30 秒后,计时器仍减慢至 1 或 2 秒的增量。我宁愿不在整个系统范围内禁用 AppNap,但我可以为这个特定的应用程序禁用它(这是我在使用另一个程序时手动运行的应用程序,需要定期与另一个程序交互,但当我退出时我会退出它我没有运行另一个)
    • 其实,无视之前的评论。如果我像以前一样添加您在AppDelegate.applicationDidFinishLaunching 中提供的代码而不是ViewController.init,它似乎工作正常。非常感谢,这似乎解决了我的问题!
    【解决方案2】:

    正如我在下面的评论中所说,如果您希望粒度低于 1.0 秒,则不应使用 Timer 对象,而应使用 GCD。我写了一个类MilliTimer,您可以在将粒度提高到几毫秒的地方使用。请在 Playground 中尝试,然后在您的应用中尝试。在这个例子中,我将基于 GCD 的定时器的粒度设置为 50 毫秒。要调整延迟,请在初始化程序的相应参数中传递您想要的延迟(以毫秒为单位)。在您的情况下,您可能对 500 ms = 0.5 s 感兴趣。

    import Cocoa
    
    public class MilliTimer
    {
        static let µseconds = 1000000.0
        static var lastUpdate = DispatchTime.now()
    
        var delay = 0
        var doStop = false
        var runs = 0
        let maxRuns = 50
    
        private class func timer(_ milliseconds:Int, closure:@escaping ()->())
        {
            let when = DispatchTime.now() + DispatchTimeInterval.milliseconds(milliseconds)
            DispatchQueue.main.asyncAfter(deadline: when, execute: closure)
        }
    
        init(delay:Int) {
            self.delay = delay
        }
    
        func delta() -> Double {
            let now = DispatchTime.now()
            let nowInMilliseconds = Double(now.uptimeNanoseconds) / MilliTimer.µseconds
            let lastUpdateInMilliseconds = Double(MilliTimer.lastUpdate.uptimeNanoseconds) / MilliTimer.µseconds
            let delta = nowInMilliseconds - lastUpdateInMilliseconds
            MilliTimer.lastUpdate = now
            return delta
        }
    
        func scheduleTimer()
        {
            MilliTimer.timer(delay) {
                print(self.delta())
                if self.doStop == false {
                    self.scheduleTimer()
                    self.runs += 1
    
                    if self.runs > self.maxRuns {
                        self.stop()
                    }
                }
            }
        }
    
        func stop() {
            doStop = true
        }
    }
    
    MilliTimer(delay: 50).scheduleTimer()
    CFRunLoopRun()
    

    【讨论】:

    • 感谢您的回答。但是,这在我的计算机上不起作用。大约 40 秒后它仍然会变慢,实际上卡住的时间要长得多。所不同的是,当它最终火灾,它触发所有的未命中的事件在同一时间:0.5000150203704834 0.5000100135803223 0.49916696548461914 10.499917984008789 5.507469177246094e-05 2.9802322387695312e-05 1.71661376953125e-05 1.3828277587890625e-05 2.9087066650390625e-05 跨度>
    • 我在操场上检查了一下,显然这个 setFireDate 的粒度是 1.0 秒,低于它显然失火的时间。我总是使用 60 秒的粒度,而且我没有任何减速。
    猜你喜欢
    • 2013-08-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-04-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多