[iOS] Timer 与 DispatchSourceTimer 如何选择与安全的使用?
使用有限状态机与 Design Patterns 封装 DispatchSourceTimer,使其更安全易用。

Photo by Ralph Hutter
关于 Timer
在 iOS 开发中一定会遇到的需求场景「Timer 定时触发器」;从 UI 层面上显示倒数计时、Banner 轮播到资料逻辑层面上的定时发送 Events、定时清除释放资料;我们都需要 Timer 来帮助我们达成目标。
Foundation — Timer (NSTimer)
Timer 应该是大家最直觉会先想到的 API,但是在选择及使用 Timer 上我们需要注意以下几个点。
优缺点
Timer 的优点:
-
默认与 UI 工作整合,不需要特别切 Main Thread 执行
-
自动调整触发时机优化使用电量
-
使用复杂度较低,最多只会发生 Retain Cycle 或忘记停止 Timer,但不会直接造成 Crash
Timer 的缺点:
-
精确度受 RunLoop 状态影响,在 UI 高互动或 Mode 切换时可能延后触发
-
不支援
suspend,resume,activate…等进阶操作
适合场景
UI 层面需求,例如轮播 Banners (Auto Scroll ScrollView)或是优惠券领取倒数计时;这些只要求使用者在前景当前画面能响应内容的场景,我会选择直接用 Timer,方便、快速、安全的达成目的。
生命周期

在 UI Main Thread 上建立 Timer,Timer 会被 Main Thread 的 RunLoop 强持有、并透过 RunLoop 轮询机制定期触发,直到 Timer invalidate( ) 才会被释放;因此我们需要在 ViewController 上强持有 Timer 并在 deinit 时呼叫 Timer invalidate( ),才能在画面退出后正确终止释放 Timer。
-
⭐️️️View Controller 强持有 Timer, Timer 的 Execution Block (handler / closure)务必为 Weak Self;否则会 Retain Cycle。
-
⭐️️️务必在 View Controller 生命周期结束时呼叫 Timer invalidate( ),否则 RunLoop 仍会持有 Timer 继续执行。
RunLoop 是 Thread 内的事件处理回圈,会轮询接收处理事件; Main Thread 系统会自动建立 RunLoop (RunLoop.main) ,除此之外其他 Thread 不一定会有 RunLoop。
使用
我们可以直接使用 Timer.scheduledTimer 宣告一个 Timer (会自动加入 RunLoop.main & Mode: .default ):
final class HomeViewController: UIViewController {
private var timer: Timer?
deinit {
self.timer?.invalidate()
self.timer = nil
}
override func viewDidLoad() {
super.viewDidLoad()
startCarousel()
}
private func startCarousel() {
self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { [weak self] _ in
self?.doSomething()
})
}
private func doSomething() {
print("Hello World!")
}
}
也可以自行宣告 Timer 物件加入到 RunLoop:
let timer= Timer(timeInterval: 1.0, repeats: true) { [weak self] _ in
// do something..
}
self.timer = timer
// 加入 RunLoop 后才会开始执行
RunLoop.main.add(timer, forMode: .default)
Timer 的操作方法
-
invalidate()终止 Timer -
fire()立即触发一次
RunLoop Mode 的影响
-
.default:预设加入的 Mode,主要是处理 UI 显示。 会在切换到.trackingMode 时先暂停 -
.tracking:处理 ScrollView 滚动、Gesture 手势。 -
.common:.default+.tracking都会处理。
⭐️️️⭐️️️⭐️️️因此在默认情况下,我们的 Timer 是加到
.defaultMode, 会在使用者滚动 ScrollView 或是手势操作时自动暂停 ,等到操作结束后才会继续,可能造成 Timer 延后触发或是次数低于预期。
对此,我们可以把 Timer 改加入到 .common Mode 就能解决以上问题:
RunLoop.main.add(timer, forMode: .common)
Grand Central Dispatch — DispatchSourceTimer
除了 Timer 之外,GCD 也提供了另一个 DispatchSourceTimer 方法可供选择。
优缺点
DispatchSourceTimer 的优点:
-
操作弹性(支援
suspend,resume) 较好 -
精确度与可靠程度较高:依赖 GCD Queue
-
可自行设定 leeway 控制耗电量
-
可稳定常驻任务 (GCD Queue)
DispatchSourceTimer 的缺点:
-
UI 操作需自行切换回 Main Thread
-
API 使用复杂且有顺序, 用错会 Crash
-
需要封装才能安全使用
适合场景
相较 Timer 适合 UI 层面的场景,DispatchSourceTimer 比较适合做那些跟 UI 或使用者当前画面无关的任务场景;最常见的就是发送 Tracking 事件,我们会定时把使用者操作产生的事件发送到伺服器,或是定时清理无用的 CoreData 资料;这些就很适合使用 DispatchSourceTimer。
生命周期

DispatchSourceTimer 的生命周期取决于是否仍被外部物件持有;GCD queue 本身不会强持有 timer 的 owner,只负责调度与执行事件。
闪退问题
DispatchSourceTimer 虽然提供更多可操作方法: activate , suspend , resume , cancel ;但是它极其敏感,只要呼叫的顺序不对就会直闪退 (EXC_BREAKPOINT/DispatchSourceTimer) 非常危险。

以下情况均会直接闪退:
-
❌ suspend( ) 与 resume( ) 没有成对使用 suspend( ) 后又呼叫一次 suspend( ) resume( ) 后又呼叫一次 resume( )
-
❌ suspend( ) 后呼叫 cancel( ) 需要先 resume( ) 才能 cancel( )
-
❌ suspend( ) 状态下 Timer 被释放 (nil)
-
❌ cancel( ) 后再呼叫其他操作
使用 Finite-State Machine 有限状态机封装操作
进入本篇文章的另一个重点,该如何安全的使用 DispatchSourceTimer?

如上图所示,我们使用有限状态机封装 DispatchSourceTimer 的操作,使其可以更安全、更容易的使用:
final class DispatchSourceTimerMachine {
// 有限状态机有哪些状态
private enum TimerState {
// 初始状态
case idle
// 执行中
case running
// 暂停中
case suspended
// 终止中
case cancelled
}
private var timer: DispatchSourceTimer?
private lazy var timerQueue: DispatchQueue = {
DispatchQueue(label: "li.zhgchg.DispatchSourceTimerMachine", qos: .background)
}()
private var _state: TimerState = .idle
deinit {
// Owner 物件消失时,同步 cancel timer
// 虽不做也不影响(handler 是 weak),但是可以确保流程符合预期
if _state == .suspended {
timer?.resume()
_state = .running
}
if _state == .running {
timer?.cancel()
timer = nil
_state = .cancelled
}
}
// 启动 Timer
func activate(repeatTimeInterval: DispatchTimeInterval, handler: DispatchSource.DispatchSourceHandler?) {
// 只有 idle, cancelled 状态可以启用 Timer
guard [.idle, .cancelled].contains(_state) else { return }
// 建立 Timer and activate()
let timer = makeTimer(repeatTimeInterval: repeatTimeInterval, handler: handler)
self.timer = timer
timer.activate()
// 切换到 running 状态
_state = .running
}
// 暂停 Timer
func suspend() {
// 只有在 running 状态可以暂停 Timer
guard [.running].contains(_state) else { return }
// 暂停 Timer
timer?.suspend()
// 切换到 suspended 状态
_state = .suspended
}
// 恢复 Timer
func resume() {
// 只有在 suspended 状态可以恢复 Timer
guard [.suspended].contains(_state) else { return }
// 恢复 Timer
timer?.resume()
// 切换到 running 状态
_state = .running
}
// 终止 Timer
func cancel() {
// 只有在 suspended, running 状态可以终止 Timer
guard [.suspended, .running].contains(_state) else { return }
// 如果当前是 suspended 状态,先 resume() 再终止
// 此为 DispatchSourceTimer 的限制,只能在 running 才能 cancel()
if _state == .suspended {
self.resume()
}
// 终止 Timer
timer?.cancel()
timer = nil
// 切换到 cancelled 状态
_state = .cancelled
}
private func makeTimer(repeatTimeInterval: DispatchTimeInterval, handler: DispatchSourceProtocol.DispatchSourceHandler?) -> DispatchSourceTimer {
let timer = DispatchSource.makeTimerSource(queue: timerQueue)
timer.schedule(deadline: .now(), repeating: repeatTimeInterval)
timer.setEventHandler(qos: .background, handler: handler)
return timer
}
}
我们简单使用有限状态机「封装了状态可以转换成什么状态」与「状态需要做什么」的逻辑,如果在错误的状态下呼叫会被忽略(不会闪退),我们还多做了一些优化,例如 suspended 状态也能 cancel、cancelled 状态能重新 activate。
延伸阅读:
之前写过另一篇文章「 Design Patterns 实战应用|封装 Socket.IO 即时通讯架构 」中也有使用到有限状态机,另外还多使用了 State Pattern。
Finite-State Machine 有限状态机: 关注的是状态之间的转换控制与该做什么。
State Pattern: 关注的是每个状态内的行为逻辑。
使用 Serial Queue 操作有限状态机状态转换
有了状态机确保 DispatchSourceTimer 能安全使用之后还没结束,我们无法保证在外部呼叫使用 DispatchSourceTimerMachine 的地方是在同个 Thread,如果不同 Thread 都操作了这个物件就会造成 Race Condition 一样会引发闪退。
final class DispatchSourceTimerMachine {
// 有限状态机有哪些状态
private enum TimerState {
// 初始状态
case idle
// 执行中
case running
// 暂停中
case suspended
// 终止中
case cancelled
}
private var timer: DispatchSourceTimer?
private lazy var timerQueue: DispatchQueue = {
DispatchQueue(label: "li.zhgchg.DispatchSourceTimerMachine", qos: .background)
}()
private var _state: TimerState = .idle
private static let operationQueueSpecificKey = DispatchSpecificKey<ObjectIdentifier>()
private lazy var operationQueueSpecificValue: ObjectIdentifier = ObjectIdentifier(self)
private lazy var operationQueue: DispatchQueue = {
let queue = DispatchQueue(label: "li.zhgchg.DispatchSourceTimerMachine.operationQueue")
queue.setSpecific(key: Self.operationQueueSpecificKey, value: operationQueueSpecificValue)
return queue
}()
private func operation(async: Bool = true, _ work: @escaping () -> Void) {
if DispatchQueue.getSpecific(key: Self.operationQueueSpecificKey) == operationQueueSpecificValue {
work()
} else {
if async {
operationQueue.async(execute: work)
} else {
operationQueue.sync(execute: work)
}
}
}
deinit {
// Owner 物件消失时,同步 cancel timer
// 虽不做也不影响(handler 是 weak),但是可以确保流程符合预期
// 确保 sync 执行完毕
operation(async: false) { [self] in
if _state == .suspended {
timer?.resume()
_state = .running
}
if _state == .running {
timer?.cancel()
timer = nil
_state = .cancelled
}
}
}
// 启动 Timer
func activate(repeatTimeInterval: DispatchTimeInterval, handler: DispatchSource.DispatchSourceHandler?) {
operation { [weak self] in
guard let self = self else { return }
// 只有 idle, cancelled 状态可以启用 Timer
guard [.idle, .cancelled].contains(_state) else { return }
// 建立 Timer and activate()
let timer = makeTimer(repeatTimeInterval: repeatTimeInterval, handler: handler)
self.timer = timer
timer.activate()
// 切换到 running 状态
_state = .running
}
}
// 暂停 Timer
func suspend() {
operation { [weak self] in
guard let self = self else { return }
// 只有在 running 状态可以暂停 Timer
guard [.running].contains(_state) else { return }
// 暂停 Timer
timer?.suspend()
// 切换到 suspended 状态
_state = .suspended
}
}
// 恢复 Timer
func resume() {
operation { [weak self] in
guard let self = self else { return }
// 只有在 suspended 状态可以恢复 Timer
guard [.suspended].contains(_state) else { return }
// 恢复 Timer
timer?.resume()
// 切换到 running 状态
_state = .running
}
}
// 终止 Timer
func cancel() {
operation { [weak self] in
guard let self = self else { return }
// 只有在 suspended, running 状态可以终止 Timer
guard [.suspended, .running].contains(_state) else { return }
// 如果当前是 suspended 状态,先 resume() 再终止
// 此为 DispatchSourceTimer 的限制,只能在 running 才能 cancel()
if _state == .suspended {
self.resume()
}
// 终止 Timer
timer?.cancel()
timer = nil
// 切换到 cancelled 状态
_state = .cancelled
}
}
private func makeTimer(repeatTimeInterval: DispatchTimeInterval, handler: DispatchSourceProtocol.DispatchSourceHandler?) -> DispatchSourceTimer {
let timer = DispatchSource.makeTimerSource(queue: timerQueue)
timer.schedule(deadline: .now(), repeating: repeatTimeInterval)
timer.setEventHandler(qos: .background, handler: handler)
return timer
}
}
现在,我们可以安全无忧的使用 DispatchSourceTimerMachine 物件作为 Timer 了:
final class TrackingEventSender {
private let timerMachine = DispatchSourceTimerMachine()
public var events: [String: String] = []
// 启动定期 tracking
func startTracking() {
timerMachine.activate(repeatTimeInterval: .seconds(30)) { [weak self] in
self?.sendTrackingEvent()
}
}
// 暂停 tracking(例如 App 进背景)
func pauseTracking() {
timerMachine.suspend()
}
// 恢复 tracking(例如 App 回前景)
func resumeTracking() {
timerMachine.resume()
}
// 停止 tracking(例如页面离开)
func stopTracking() {
timerMachine.cancel()
}
private func sendTrackingEvent() {
// send events to server...
}
}
到此如何安全的使用 DispatchSourceTimer 环节已结束,再来延伸几个 Design Patterns 的使用,方便我们抽象物件进行测试跟 DispatchSourceHandler 执行逻辑抽象。
延伸 — 使用 Adapter Pattern + Factory Pattern 产生 DispatchSourceTimer (利于抽象测试)
DispatchSourceTimer 是 GCD 的 Objective-C 物件,在测试环节我们很难对其 Mock (无 Protocol);因此我们需要自己定义一层 Protocol + Factory Pattern 产生,让 TimerStateMachine 是能写测试的。
Adapter Pattern— 封装 DispatchSourceTimer 操作:
public protocol TimerAdapter {
func schedule(repeating: DispatchTimeInterval)
func setEventHandler(handler: DispatchSourceProtocol.DispatchSourceHandler?)
func activate()
func suspend()
func resume()
func cancel()
}
// DispatchSourceTimer 的 Adapter 实现
final class DispatchSourceTimerAdapter: TimerAdapter {
// 原始的 DispatchSourceTimer
private let timer: DispatchSourceTimer
init(label: String = "li.zhgchg.DispatchSourceTimerAdapter") {
let queue = DispatchQueue(label: label, qos: .background)
let timer = DispatchSource.makeTimerSource(queue: queue)
self.timer = timer
}
func schedule(repeating: DispatchTimeInterval) {
timer.schedule(deadline: .now(), repeating: repeating)
}
func setEventHandler(handler: DispatchSourceProtocol.DispatchSourceHandler?) {
timer.setEventHandler(qos: .background, handler: handler)
}
func activate() {
timer.activate()
}
func suspend() {
timer.suspend()
}
func resume() {
timer.resume()
}
func cancel() {
timer.cancel()
}
}
Factory Pattern — 抽象产生 TimerAdapter 的方法:
protocol DispatchSourceTimerAdapterFactorySpec {
func makeTimer(repeatTimeInterval: DispatchTimeInterval, handler: DispatchSourceProtocol.DispatchSourceHandler?) -> TimerAdapter
}
// 封装 DispatchSourceTimerAdapter 产生步骤
final class DispatchSourceTimerAdapterFactory: DispatchSourceTimerAdapterFactorySpec {
public func makeTimer(repeatTimeInterval: DispatchTimeInterval, handler: DispatchSourceProtocol.DispatchSourceHandler?) -> TimerAdapter {
let timer = DispatchSourceTimerAdapter()
timer.schedule(repeating: repeatTimeInterval)
timer.setEventHandler(handler: handler)
return timer
}
}
组合使用:
var stateMachine = DispatchSourceTimerMachine(timerFactory: DispatchSourceTimerAdapterFactory())
//
final class DispatchSourceTimerMachine {
// 略..
private var timer: TimerAdapter?
private let timerFactory: DispatchSourceTimerAdapterFactorySpec
public init(timerFactory: DispatchSourceTimerAdapterFactorySpec) {
self.timerFactory = timerFactory
}
// 略..
func activate(repeatTimeInterval: DispatchTimeInterval, handler: DispatchSource.DispatchSourceHandler?) {
onQueue { [weak self] in
guard let self else { return }
guard [.idle, .cancelled].contains(_state) else { return }
// 使用 Factory MakeTimer
let timer = timerFactory.makeTimer(repeatTimeInterval: repeatTimeInterval, handler: handler)
self.timer = timer
timer.activate()
_state = .running
}
}
// 略..
}
这样我们就能对 TimerAdapter / DispatchSourceTimerAdapterFactorySpec 在测试环节撰写 Mock Object 跑单元测试。
延伸 — 使用 Strategy Pattern 封装 DispatchSourceHandler 工作
假设我们的 DispatchSourceHandler 希望执行的事能动态改变,可以使用 Strategy Pattern 来封装工作内容。
TrackingHandlerStrategy:
protocol TrackingHandlerStrategy {
static var target: String { get }
func execute()
}
// Home Event
final class HomeTrackingHandlerStrategy: TrackingHandlerStrategy {
static var target: String = "home"
func execute() {
// fetch home event logs..and send
}
}
// Product Event
final class ProductTrackingHandlerStrategy: TrackingHandlerStrategy {
static var target: String = "product"
func execute() {
// fetch product event logs..and send
}
}
组合使用:
var sender = TrackingEventSender()
sender.register(event: HomeTrackingHandlerStrategy())
sender.register(event: ProductTrackingHandlerStrategy())
sender.startTracking()
// ...
//
final class TrackingEventSender {
private let timerMachine = DispatchSourceTimerMachine()
private var events: [String: TrackingHandlerStrategy] = [:]
// 注册需要的 Event 策略
func register(event: TrackingHandlerStrategy) {
events[type(of: event).target] = event
}
func retrive<T: TrackingHandlerStrategy>(event: T.Type) -> T? {
return events[event.target] as? T
}
// 启动定期 tracking
func startTracking() {
timerMachine.activate(repeatTimeInterval: .seconds(30)) { [weak self] in
self?.events.values.forEach { event in
event.execute()
}
}
}
// 暂停 tracking(例如 App 进背景)
func pauseTracking() {
timerMachine.suspend()
}
// 恢复 tracking(例如 App 回前景)
func resumeTracking() {
timerMachine.resume()
}
// 停止 tracking(例如页面离开)
func stopTracking() {
timerMachine.cancel()
}
}
鸣谢
感谢 Ethan Huang 大大 Donate 的 5 Beers :
的确快半年没写什么了,新工作刚到职,持续找寻灵感中!💪
下一篇可能分享 Fastlane Match 凭证管理跟 Self-hosted Runner 的建置过程. .或是 Bitbucket Pipeline. .或是 AppStoreConnect API…




留言 · Comments