為什么要封裝一個Timer
- 項目中經(jīng)常用到, 并且一不留神就會造成循環(huán)引用
- 項目需要展示定時器有效的運行時間
為什么選擇GCD Timer
Timer
- Timer其實就是CFRunLoopTimerRef, 他們之間是toll-free bridged;
- 一個Timer注冊到RunLoop后, RunLoop會為其重復(fù)的時間點注冊好事件,例如01:00仔拟、01:10這幾個時間點;RunLoop為了節(jié)省資源,并不會在非常準確的時間點回調(diào)這個Timer;
- Timer有個屬性叫做Tolerance(寬容度),表示了當前時間點到后,允許有多少誤差;
- 由于Timer的這種機制,因此Timer的執(zhí)行必須依賴于RunLoop,如果沒有RunLoop則Timer不會執(zhí)行, 如果RunLoop任務(wù)過于繁重, 可能就會導致Timer不準時;
- 若加入RunLoop時設(shè)置的不是commonModes這個集合,也會受到影響;
CADisplayLink
- CADisplayLink是一個執(zhí)行頻率(fps)和屏幕刷新相同(可以修改preferredFramesPerSeconf改變刷新頻率)的定時器,它也需要加入RunLoop才能執(zhí)行;
- 與NSTimer類似, CADisplayLink同樣基于CFRunLoopTimerRef實現(xiàn), 底層使用mk_timer;
- 與Timer相比它的精度更高,不過和Timer類似的是如果遇到大任務(wù),仍然存在丟幀現(xiàn)象; 通常情況下CADisplayLink用于構(gòu)建幀動畫,看起來更加流暢;
GCD Timer
- GCD則不同, GCD的線程管理是通過系統(tǒng)直接管理的, GCD Timer是通過dispatch port給RunLoop發(fā)送消息,來使RunLoop執(zhí)行相應(yīng)的block, 如果所在線程沒有RunLoop, 那么GCD會臨時創(chuàng)建一個線程去執(zhí)行block,執(zhí)行完之后銷毀,因此GCD的Timer是不依賴RunLoop的;
- 由于GCD Timer是通過port發(fā)送消息的機制來觸發(fā)RunLoop的,如果RunLoop阻塞了, 還是會存在延遲的;
代碼
執(zhí)行方法
/**
* startTime: 開始時間, 默認立即開始
* interval: 間隔時間, 默認1s
* isRepeats: 是否重復(fù)執(zhí)行, 默認true
* isAsync: 是否異步, 默認false
* task: 執(zhí)行任務(wù)
*/
class func execTask(startTime: TimeInterval = 0, interval: TimeInterval = 1, isRepeats: Bool = true, isAsync: Bool = false, task: @escaping ((_ duration: Int) -> Void)) -> String? {
if (interval <= 0 && isRepeats) || startTime < 0 {
return nil
}
let queue = isAsync ? DispatchQueue(label: "GCDTimer") : DispatchQueue.main
let timer = DispatchSource.makeTimerSource(flags: [], queue: queue)
timer.schedule(deadline: .now() + startTime, repeating: 1.0, leeway: .milliseconds(0))
semphore.wait()
let name = "\(GCDTimer.timers.count)"
timers[name] = timer
timersState[name] = GCDTimerState.running
durations[name] = 0
fireTimes[name] = Date().timeIntervalSince1970
semphore.signal()
timer.setEventHandler {
var lastTotalTime = durations[name] ?? 0
let fireTime = fireTimes[name] ?? 0
lastTotalTime = lastTotalTime + Date().timeIntervalSince1970 - fireTime
task(lround(lastTotalTime))
if !isRepeats {
self.cancelTask(task: name)
}
}
timer.activate()
return name
}
執(zhí)行方法會返回一個任務(wù)字符串, 用于外界直接取消、暫停等操作
// 使用默認值
task1 = GCDTimer.execTask(task: { (totalTimer) in
print("定時器運行有效時間(暫停時間不會計入): \(totalTimer)")
})
task2 = GCDTimer.execTask(startTime: 1, interval: 2, isRepeats: true, isAsync: false) { (_ ) in
print("1s后開始, 定時器間隔2s, 允許重復(fù)執(zhí)行, 不開啟子線程")
}
取消定時器
class func cancelTask(task: String?) {
guard let _task = task else {
return
}
semphore.wait()
if timersState[_task] == .suspend {
resumeTask(task: _task)
}
getTimer(task: _task)?.cancel()
if let state = timersState.removeValue(forKey: _task) {
print("The value \(state) was removed.")
}
if let timer = timers.removeValue(forKey: _task) {
print("The value \(timer) was removed.")
}
if let fireTime = fireTimes.removeValue(forKey: _task) {
print("The value \(fireTime) was removed.")
}
if let duration = durations.removeValue(forKey: _task) {
print("The value \(duration) was removed.")
}
semphore.signal()
}
將開啟定時器時反的task1/task2傳入即可
GCDTimer.cancelTask(task: task1)
暫停
class func suspendTask(task: String?) {
guard let _task = task else {
return
}
if timersState.keys.contains(_task) {
timersState[_task] = .suspend
getTimer(task: _task)?.suspend()
var lastTotalTime = durations[_task] ?? 0
let fireTime = fireTimes[_task] ?? 0
lastTotalTime = lastTotalTime + Date().timeIntervalSince1970 - fireTime
durations[_task] = lastTotalTime
}
}
調(diào)用方式同取消定時器
恢復(fù)定時器
class func resumeTask(task: String?) {
guard let _task = task else {
return
}
if timersState.keys.contains(_task) && timersState[_task] != .running {
fireTimes[_task] = Date().timeIntervalSince1970
getTimer(task: task)?.resume()
timersState[_task] = .running
}
}
GCD Timer的resume與suspend是成對出現(xiàn)的, 所以不能重復(fù)resume