iOS Timer 与 DispatchSourceTimer 选择与安全封装技巧|有限状态机防止闪退
iOS 开发必备 Timer 使用指南,解析 Timer 与 DispatchSourceTimer 优缺点,并提供有限状态机封装 DispatchSourceTimer,避免闪退及 Race Condition,实现高精度且安全的定时任务管理。
Click here to view the English version of this article.
點擊這裡查看本文章正體中文版本。
基于 SEO 考量,本文标题与描述经 AI 调整,原始版本请参考内文。
文章目录
[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 ):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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:
1
2
3
4
5
6
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()终止 Timerfire()立即触发一次
RunLoop Mode 的影响
.default:预设加入的 Mode,主要是处理 UI 显示。 会在切换到.trackingMode 时先暂停.tracking:处理 ScrollView 滚动、Gesture 手势。.common:.default+.tracking都会处理。
⭐️️️⭐️️️⭐️️️因此在默认情况下,我们的 Timer 是加到
.defaultMode, 会在使用者滚动 ScrollView 或是手势操作时自动暂停 ,等到操作结束后才会继续,可能造成 Timer 延后触发或是次数低于预期。
对此,我们可以把 Timer 改加入到 .common Mode 就能解决以上问题:
1
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 的操作,使其可以更安全、更容易的使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
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 一样会引发闪退。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
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 了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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 操作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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 的方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
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
}
}
组合使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
}
}
组合使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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…
延伸阅读
有任何问题及指教欢迎 与我联络 。
本文首次发表于 Medium (点击查看原始版本),由 ZMediumToMarkdown 提供自动转换与同步技术。





