Created
December 14, 2022 18:31
-
-
Save VaslD/43ceed878fb7fd14db7668e5a4d435c4 to your computer and use it in GitHub Desktop.
基于 DispatchSource 的正计时器。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
/// 递增(正)计时器,基于 `DispatchSource` 和 `CLOCK_MONOTONIC_RAW`,必须在主线程使用。 | |
@MainActor | |
public class CountingTimer { | |
/// 计时原点:创建时刻或上次重置时刻。此数值为系统底层时钟周期,只能通过多次获取后计算时长(单位:纳秒)而不能用于确定当前日期时间。 | |
public private(set) var origin: UInt64 | |
/// 回调间隔。修改间隔时间将以当前时刻重新计算间隔,而非顺延上次回调后已经经过的时间。 | |
public var interval: TimeInterval { | |
didSet { | |
guard self.interval != oldValue else { return } | |
self.timer.cancel() | |
self.scheduleTimer() | |
if self.checkpoint == nil { | |
self.timer.resume() | |
} | |
} | |
} | |
/// 创建计时器。默认暂停,需要调用 ``start()`` 开始计时和回调。 | |
/// | |
/// - Parameters: | |
/// - interval: 回调间隔。 | |
/// - handler: 回调闭包。计时器持有并强引用回调闭包,如果需要在闭包中引用同时持有计时器的对象,必须切换为弱引用。 | |
/// - timer: 计时器实例。 | |
/// - timeElapsed: 已经过的时间,单位:秒。等同于当前时刻减去计时原点 (``origin``) 和暂停时长 (``offset``)。 | |
public init(interval: TimeInterval, | |
handler: @escaping (_ timer: CountingTimer, _ timeElapsed: TimeInterval) -> Void) { | |
self.origin = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW) | |
self.checkpoint = self.origin | |
self.interval = interval | |
self.handler = handler | |
self.scheduleTimer() | |
} | |
deinit { | |
// FIXME: https://forums.swift.org/t/deinit-and-mainactor/50132/5 | |
self.timer.cancel() | |
// DispatchSource 释放时不能处于暂停状态(即使已经取消) | |
// Bug in client of libDispatch: "Release of a suspended object" | |
if self.checkpoint != nil { | |
self.timer.resume() | |
} | |
} | |
// MARK: 计时器 | |
var timer: DispatchSourceTimer! | |
func scheduleTimer() { | |
let timer = DispatchSource.makeTimerSource(queue: .main) | |
if let seconds = Int(exactly: self.interval) { | |
timer.schedule(deadline: .now(), repeating: .seconds(seconds)) | |
} else { | |
timer.schedule(deadline: .now(), repeating: .milliseconds(Int(self.interval * 1000))) | |
} | |
timer.setEventHandler { [weak self, weak timer] in | |
guard let self = self else { | |
timer?.cancel() | |
return | |
} | |
guard self.shouldCallHandler else { return } | |
let moment = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW) | |
let elapsed = Double(Int64(moment - self.origin) - self.offset) / 1e+9 | |
self.handler(self, elapsed) | |
} | |
self.timer = timer | |
} | |
/// 暂停原点。 | |
var checkpoint: UInt64? | |
/// 总偏移时长。仅在非暂停期间(``isSuspended`` 为 `false`)更新;暂停期间获取的数值可能滞后(过短)或无意义。 | |
public private(set) var offset: Int64 = 0 | |
/// 计时器是否处于暂停状态。已暂停的计时器不会回调;所经过的时间也不计入计时时长。 | |
public var isSuspended: Bool { self.checkpoint != nil } | |
/// 启动或恢复计时。如果曾经调用过 ``disableCallbacks()``,不会自动重启回调。重复启动正在运行的计时器无意义。 | |
@discardableResult | |
public func start() -> CountingTimer { | |
guard let checkpoint = self.checkpoint else { return self } | |
let moment = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW) | |
self.offset += Int64(moment - checkpoint) | |
self.checkpoint = nil | |
self.timer.resume() | |
return self | |
} | |
/// 暂停计时和回调。可通过调用 ``start()`` 恢复。 | |
@discardableResult | |
public func stop() -> CountingTimer { | |
self.checkpoint = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW) | |
self.timer.suspend() | |
return self | |
} | |
/// 切换启停状态。等同于检查 ``isSuspended`` 并调用 ``start()`` 或 ``stop()``。 | |
public func toggle() { | |
if self.checkpoint == nil { | |
self.stop() | |
} else { | |
self.start() | |
} | |
} | |
/// 停止并重置计时器,包括计时原点、暂停时长、回调开关等。 | |
@discardableResult | |
public func reset() -> CountingTimer { | |
self.stop() | |
self.origin = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW) | |
self.checkpoint = self.origin | |
self.shouldCallHandler = true | |
return self | |
} | |
// MARK: 微调 | |
/// 手动设置计时时长,影响后续回调的时长计算。此方法几乎没有合理的使用场景,请谨慎调用;传参可能将计时器回退至过去的时刻,导致重复回调。 | |
/// | |
/// - Parameter duration: 已经过的时长,单位:秒。 | |
@available(*, deprecated, message: "此方法可能将计时器回退至过去的时刻,导致重复回调同一时长。仅当明确允许此类行为时使用。") | |
public func setTimeElapsed(_ duration: TimeInterval) { | |
let moment = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW) | |
let duration = Int64(duration * 1e+9) | |
self.offset = Int64(moment - self.origin) - duration | |
} | |
// MARK: 回调 | |
var shouldCallHandler = true | |
var handler: (_ timer: CountingTimer, _ timeElapsed: TimeInterval) -> Void | |
/// 修改回调闭包。 | |
public func setHandler(_ handler: @escaping (_ timer: CountingTimer, _ timeElapsed: TimeInterval) -> Void) { | |
self.handler = handler | |
} | |
/// 启用回调(默认值)。 | |
public func enableCallbacks() { | |
self.shouldCallHandler = true | |
} | |
/// 关闭回调,但不暂停计时器。必须通过 ``enableCallbacks()`` 重新启用回调;暂停和恢复计时器不会影响回调开关。 | |
public func disableCallbacks() { | |
self.shouldCallHandler = false | |
} | |
/// 切换回调开关。等同于检查回调状态并调用 ``enableCallbacks()`` 或 ``disableCallbacks()``。 | |
public func toggleCallbacks() { | |
self.shouldCallHandler = !self.shouldCallHandler | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment