重学GCD之 DispatchSourceTimer
iOS 中上层的定时器API以及它们的问题
通常来说, 我们在使用iOS 的Timer
或者 GCD 的 DispatchAfter
的API, 他们都会有或多或少的问题.
Timer
的生命周期管理问题, Timer的事件触发依赖Runloop
DispatchAfter
会在指定时间以后才加入到DispatchQueue中. 他们的定时准确度都比较差OC
的performxxx
API本质底层是一个Timer
这里就不展开了, 网上有很多文章都有说明.
更好的Timer - DispatchSourceTimer
GCD中DispatchSource
相关的API是iOS针对kqueue
事件源的封装. kqueue
作为Apple内核支持的事件驱动方式, 具体的api和功能与epoll
类似. 我们可以借助DispatchSource
来实现精度更高的定时器, 这里我们称为GCD Timer
通过系统的API. 我们知道GCD Timer
支持的功能非常棒, 它更优秀体现在如下:
- 支持延迟启动, 支持Repeat, 支持 leeyway 触发精度
- 支持 Suspend, Resume
- 支持第一次触发的 notify
- 支持指定event触发的 dispatchQueue
- 支持cancel
但是, 它也有一些问题!!! 具体来说包括如下问题:
suspend/resume
必须成对出现, 否则GCD Timer内部状态紊乱非常容易Crash- 在
suspend/resume
状态下调用timer.cancel
也会导致crash - 生命周期管理需要注意!!!
GCD Timer的封装
因此, 这里针对以上的问题, 对GCD Timer 进行了封装, 注意resume/suspend
方法必须在同一个线程中调用:
//
// RepeatingTimer.swift
// LearnSwiftDispatchItem
//
// Created by brownfeng on 2022/4/27.
//
import Foundation
// 实现的原因是:
// 1. 需要 suspend/resume
// 2. 不同状态下 timer = nil 会 crash!!!
class RepeatingTimer {
var registrationHandler: (() -> Void)?
var eventHandler: (() -> Void)?
var workQueue: DispatchQueue?
init(timeInterval: TimeInterval) {
self.timeInterval = timeInterval
}
// We should also ensure its accessed from the same thread/queue
// 防止 data race
public func resume() {
if state == .resumed {
return
}
state = .resumed
timer.resume()
}
public func suspend() {
if state == .suspended {
return
}
state = .suspended
timer.suspend()
}
// MARK: - Private Properties
private let timeInterval: TimeInterval
private lazy var timer: DispatchSourceTimer = {
let t = DispatchSource.makeTimerSource(flags: [], queue: workQueue)
t.schedule(deadline: .now() + self.timeInterval, repeating: self.timeInterval)
t.setEventHandler(handler: { [weak self] in
self?.eventHandler?()
})
t.setRegistrationHandler { [weak self] in
self?.registrationHandler?()
}
return t
}()
private enum State {
case suspended
case resumed
}
private var state: State = .suspended
deinit {
timer.setEventHandler {}
timer.cancel()
/*
If the timer is suspended, calling cancel without resuming
triggers a crash. This is documented here
https://forums.developer.apple.com/thread/15902
*/
resume()
eventHandler = nil
}
}