第11章:计时器¶
重复和非重复计时器在编码时总是有用的。 除了异步执行代码之外,您通常还需要控制任务应该重复的何时
和频率
。
在Dispatch
框架可用之前,开发人员依靠RunLoop
异步执行任务并实现并发。您可以使用Timer
创建重复和非重复计时器。然后,苹果发布了Dispatch
框架,包括DispatchSourceTimer
。
虽然上述所有内容都能够创建计时器,但并非所有计时器在Combine
中都是相等的。继续阅读!
使用RunLoop
¶
主线程和您创建的任何线程,最好使用Thread
类,都可以有自己的RunLoop
。只需从当前线程中调用RunLoop.current
:如果需要,Foundation
将为您创建一个。请注意,除非您了解运行循环的操作方式——特别是您需要运行运行循环的循环——否则您最好使用运行应用程序主线程的主RunLoop
。
Note
苹果文档中的一个重要注意事项和红灯警告是,RunLoop
类不是线程安全的。您只应该为当前线程的运行循环调用RunLoop
方法。
RunLoop
实现您将在第17章“调度程序”中了解的Scheduler
协议。它定义了几种相对低级的方法,也是唯一允许您创建可取消计时器的方法:
let runLoop = RunLoop.main
let subscription = runLoop.schedule(
after: runLoop.now,
interval: .seconds(1),
tolerance: .milliseconds(100)
) {
print("Timer fired")
}
此计时器不会传递任何值,也不会创建发布者。它从after:
带有指定interval
和tolerance
的参数中指定的日期开始,仅此而已。它与Combine
的唯一有用之处是,它返回的Cancellable
允许您在一段时间后停止计时器。
这方面的一个例子可能是:
runLoop.schedule(after: .init(Date(timeIntervalSinceNow: 3.0))) {
subscription.cancel()
}
但考虑到所有因素,RunLoop
并不是创建计时器的最佳方式。使用Timer
类会更好!
使用计时器类¶
Timer
早在苹果将其重命名为“macOS”
之前,是原始Mac OS X
中可用的最古老的计时器。由于其委托模式和与RunLoop
的密切关系,它一直很棘手。Combine
带来了一个现代变体,您可以直接用作发布者,而无需所有设置样板。
您可以通过以下方式创建重复计时器发布者:
let publisher = Timer.publish(every: 1.0, on: .main, in: .common)
确定的两个参数:
- 您的计时器连接到哪个
RunLoop
。在这里,主线程的RunLoop
。 - 计时器在哪个运行循环模式下运行。在这里,默认运行循环模式。
除非您了解运行循环是如何运行的,否则您应该坚持使用这些默认值。 运行循环是macOS
中异步事件源处理的基本机制,但它们的API
有点繁琐。 您可以通过调用RunLoop.current
为您自己创建或从Foundation
获取的任何线程获取RunLoop
,因此您也可以编写以下代码:
let publisher = Timer.publish(every: 1.0, on: .current, in: .common)
Note
在DispatchQueue.main
以外的派单队列上运行此代码可能会导致不可预测的结果。Dispatch
框架在不使用运行循环的情况下管理其线程。由于运行循环需要调用其run
方法之一来处理事件,因此除了主队列外,您永远不会在任何队列上看到计时器触发。保持安全,为您的计时器锁定RunLoop.main
。
计时器返回的发布者是ConnectablePublisher
。这是Publisher
的一个特殊变体,在您显式调用其connect()
方法之前,它不会在订阅时开始启动。您还可以使用autoconnect()
操作符,该操作符在第一个订阅者订阅时自动连接。
Note
您将在第13章“资源管理”中了解有关可连接发布者的更多信息。
因此,创建订阅时启动计时器的发布者的最佳方法是编写:
let publisher = Timer
.publish(every: 1.0, on: .main, in: .common)
.autoconnect()
计时器反复发出当前日期,其Publisher.Output
类型为Date
。您可以使用scan
操作符制作一个发送增加值的计时器:
let subscription = Timer
.publish(every: 1.0, on: .main, in: .common)
.autoconnect()
.scan(0) { counter, _ in counter + 1 }
.sink { counter in
print("Counter is \(counter)")
}
还有一个您在这里没有看到的Timer.publish()
参数:tolerance
。它指定了与您要求的持续时间的可接受的偏差,作为TimeInterval
。但请注意,使用低于RunLoop minimumTolerance
值的值可能不会产生预期结果。
使用DispatchQueue
¶
您可以使用派单队列生成计时器事件。虽然调度框架有一个DispatchTimerSource
事件源,但令人惊讶的是,Combine
没有为它提供计时器接口。相反,您将使用另一种方法在队列中生成计时器事件。然而,这可能会有点复杂:
let queue = DispatchQueue.main
// 1
let source = PassthroughSubject<Int, Never>()
// 2
var counter = 0
// 3
let cancellable = queue.schedule(
after: queue.now,
interval: .seconds(1)
) {
source.send(counter)
counter += 1
}
// 4
let subscription = source.sink {
print("Timer emitted \($0)")
}
在之前的代码中,您:
- 创建一个
subject
,您将向其发送计时器值。 - 准备一个计数器。每次计时器工作时,你都会增加它。
- 每秒对所选队列安排重复操作。操作立即开始。
- 订阅
subject
以获取计时器值。
正如你所看到的,这并不漂亮。这有助于将此代码移动到函数,并传递间隔和开始时间。
关键点¶
- 如果您怀念
Objective-C
代码,请使用良好的旧RunLoop
类创建计时器。 - 使用
Timer.publish
获取在指定的RunLoop
上以给定间隔生成值的发布者。 - 使用
DispatchQueue.schedule
在调度队列上发出事件的现代计时器。
接下来去哪?¶
在第18章“自定义发布者和处理背压”中,您将学习如何编写自己的发布者,并将使用DispatchSourceTimer
创建替代计时发布者。
但别着急!在此之前,有很多东西需要学习,从下一章的键值观察开始。