[iOS] Timer 與 DispatchSourceTimer 如何選擇與安全的使用?
TL;DR
本文比較 iOS 中 Timer 與 DispatchSourceTimer 的優缺點及適用場景,並提出使用有限狀態機封裝 DispatchSourceTimer 以確保安全使用,避免因操作順序錯誤導致的閃退問題。
Key Takeaways
- •Timer 適合 UI 層面需求(如輪播、倒數計時),但精確度受 RunLoop 影響;DispatchSourceTimer 適合後台任務(如發送事件、清理資料),精確度較高但操作複雜易閃退。
- •使用有限狀態機封裝 DispatchSourceTimer,透過狀態轉換控制(idle、running、suspended、cancelled)確保操作順序正確,避免閃退。
- •結合 Serial Queue 操作狀態機,防止多執行緒競爭條件(Race Condition),進一步提升安全性。
- •透過 Adapter Pattern 和 Factory Pattern 抽象化 DispatchSourceTimer,便於單元測試;使用 Strategy Pattern 封裝事件處理邏輯,增加靈活性。
- •Timer 需注意生命週期管理(如使用 weak self 避免循環引用、在 deinit 呼叫 invalidate),而 DispatchSourceTimer 需嚴格遵循操作順序(如 suspend/resume 成對使用)。
使用有限狀態機與 Design Patterns 封裝 DispatchSourceTimer,使其更安全易用。
https://medium.com/media/488577f1a34898594b6da98d4e710915/href
關於 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 顯示。
會在切換到 .tracking Mode 時先暫停 - .tracking:處理 ScrollView 滾動、Gesture 手勢。
- .common:.default + .tracking 都會處理。
⭐️️️⭐️️️⭐️️️因此在默認情況下,我們的 Timer 是加到 .default Mode,會在使用者滾動 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…
延伸閱讀
- Design Patterns 的實戰應用紀錄 (封裝 Sockiet.io)
- Design Patterns 的實戰應用紀錄 (封裝 WKWebView)
- Visitor Pattern in Swift
- Visitor Pattern in TableView
有任何問題及指教歡迎與我聯絡。
https://medium.com/media/8652ffa596b15d4e2a3dc94591c00522/href[iOS] Timer 與 DispatchSourceTimer 如何選擇與安全的使用? was originally published in ZRealm Dev. on Medium, where people are continuing the conversation by highlighting and responding to this story.