Skip to content

Instantly share code, notes, and snippets.

@VaslD
Created December 14, 2022 18:31
Show Gist options
  • Save VaslD/43ceed878fb7fd14db7668e5a4d435c4 to your computer and use it in GitHub Desktop.
Save VaslD/43ceed878fb7fd14db7668e5a4d435c4 to your computer and use it in GitHub Desktop.
基于 DispatchSource 的正计时器。
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