一个不用担心循环引用的Timer
2018-09-13 本文已影响0人
22点的夜生活
为什么要封装一个Timer
- 项目中经常用到, 并且一不留神就会造成循环引用
- 项目需要展示定时器有效的运行时间
为什么选择GCD Timer
Timer
- Timer其实就是CFRunLoopTimerRef, 他们之间是toll-free bridged;
- 一个Timer注册到RunLoop后, RunLoop会为其重复的时间点注册好事件,例如01:00、01:10这几个时间点;RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer;
- Timer有个属性叫做Tolerance(宽容度),表示了当前时间点到后,允许有多少误差;
- 由于Timer的这种机制,因此Timer的执行必须依赖于RunLoop,如果没有RunLoop则Timer不会执行, 如果RunLoop任务过于繁重, 可能就会导致Timer不准时;
- 若加入RunLoop时设置的不是commonModes这个集合,也会受到影响;
CADisplayLink
- CADisplayLink是一个执行频率(fps)和屏幕刷新相同(可以修改preferredFramesPerSeconf改变刷新频率)的定时器,它也需要加入RunLoop才能执行;
- 与NSTimer类似, CADisplayLink同样基于CFRunLoopTimerRef实现, 底层使用mk_timer;
- 与Timer相比它的精度更高,不过和Timer类似的是如果遇到大任务,仍然存在丢帧现象; 通常情况下CADisplayLink用于构建帧动画,看起来更加流畅;
GCD Timer
- GCD则不同, GCD的线程管理是通过系统直接管理的, GCD Timer是通过dispatch port给RunLoop发送消息,来使RunLoop执行相应的block, 如果所在线程没有RunLoop, 那么GCD会临时创建一个线程去执行block,执行完之后销毁,因此GCD的Timer是不依赖RunLoop的;
- 由于GCD Timer是通过port发送消息的机制来触发RunLoop的,如果RunLoop阻塞了, 还是会存在延迟的;
代码
执行方法
/**
* startTime: 开始时间, 默认立即开始
* interval: 间隔时间, 默认1s
* isRepeats: 是否重复执行, 默认true
* isAsync: 是否异步, 默认false
* task: 执行任务
*/
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
}
执行方法会返回一个任务字符串, 用于外界直接取消、暂停等操作
// 使用默认值
task1 = GCDTimer.execTask(task: { (totalTimer) in
print("定时器运行有效时间(暂停时间不会计入): \(totalTimer)")
})
task2 = GCDTimer.execTask(startTime: 1, interval: 2, isRepeats: true, isAsync: false) { (_ ) in
print("1s后开始, 定时器间隔2s, 允许重复执行, 不开启子线程")
}
取消定时器
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
}
}
调用方式同取消定时器
恢复定时器
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是成对出现的, 所以不能重复resume