![62db642c4c07d021e1211cdf2b47e569.png](https://i-blog.csdnimg.cn/blog_migrate/bc399ddd1df22e43f9dd5f58aa1b377a.jpeg)
前段时间开发了一款状态栏小工具 时光(Time Go) ,它用到了定时器,近期十里在想大家会不会也有用定时器的需求呢!所以呢,写本文意在分享 macOS 开发中关于定时器的的收获及其使用方法,我们一起进步。
实现平台
- macOS 10.14.3
- swift 4.2.1
- xcode 10.1
1. 准备环境
为了更好地展示定时器的使用,这里我们使用 playground 测试我们的定时器,可以使用快捷键 Option
+ Shift
+ Command
+ n 新建 playgroud 文件。在文件中我们准备一个 TimerDemo 类用于测试定时器:
import Foundation
class TimerDemo {
private var timeCount = 5
static let share = TimerDemo()
// 添加不同定时器使用方法的示例代码
func run() {
print("TimerDemo")
}
}
TimerDemo.share.run()
上面定义了 TimerDemo 类,同时实现了一个 TimerDemo 的单例,方便调用方法展示效果!
2. 定时器的使用
网上能搜到有三种实现定时器的方式,因为苹果官方 API 的变化导致部分搜到的信息过时,本文整理的实现方法适用于上述描述的实现平台,近期应该不会过期,哈哈:
- Timer 类实现
- GCD(Grand Central Dispatch) 的方式
- 利用屏幕刷新实现计数定时
本人觉得第三种方式不是什么好的方式,所以本文只讲前两种方式。
2.1 使用 Timer 类
Timer 类的使用依赖 RunLoop(线程的事件循环,app 运行一定会有一个名为 main 的 RunLoop,主要负责界面控件的显示和交互) ,只有添加到激活状态的 RunLoop 中定时器才能正常工作。
定时器的运行根据次数分两种:单次和循环:
- 单次的定时器,会在指定的时间过后,自动销毁并从 RunLoop 中弹出,而不再影响 RunLoop 的运行
- 循环的定时器,会根据指定时间间隔触发执行处理任务,需要开发者手动执行 Timer 对象的
invalidate
方法才能销毁并从 RunLoop 中弹出。一般使用循环定时器,不会让其一直执行,满足某种条件或执行指定次数之后,手动调用 invlaidate 方法停止定时器
根据定时器的使用方式,可以分三种:
- 使用
Timer(timeInterval:repeats:block:)
或Timer(timeInterval:target:selector:userInfo:repeats:)
类方法初始化 Timer 对象,将对象手动添加到指定 RunLoop 中执行 - 使用
Timer(fire:interval:repeats:block:)
或Timer(fireAt:interval:target:selector:userInfo:repeats:)
类方法初始化 Timer 对象,将对象手动添加到指定 RunLoop 中执行 - 使用
Timer.scheduledTimer(withTimeInterval:repeats:block:)
和Timer.scheduledTimer(timeInterval:target:selector:userInfo:repeats:)
类方法,这两个方法都会创建 Timer 对象,并且以默认的运行模式添加到当前的 RunLoop 中
下面我们开始尝试上面的三种方式。先在上面定义的 TimerDemo 类中声明一个 timer 属性:
private var timer: Timer?
2.1.1 第一种方式
2.1.1.1 Timer(timeInterval:repeats:block:)
类方法
- timeInterval 是定时器的触发时间间隔,单位为秒
- repeats 布尔类型,代表是否重复,如果为 true ,定时器是循环定时器,如果为 false ,定时器是单次的
- block 为
((timer: Timer?) -> Void)
类型的闭包,其中 timer 是定时器自身
我们为 TimerDemo 类定义一个 oneshot1
方法,使用 Timer(timeInterval:repeats:block:)
类方法实现一个单次的定时器,如下:
// 使用 Timer(timeInterval:repeats:block:) 初始化对象,并将其添加到 main 线程的 RunLoop 中,单次
func oneshot1() {
print("(Date()): Timer(timeInterval:repeats:block:) 初始化,单次")
timer = Timer(timeInterval: 1, repeats: false, block: { timer in
self.oneFireHandler(timer)
})
RunLoop.main.add(timer!, forMode: .common)
}
可以看到定时器时间间隔是 1 秒,单次,并且闭包中调用定义的 oneFireHandler
方法,此方法是用来处理我们需要做的定时任务的,比如我们打印一个字符串:
@objc private func oneFireHandler(_ timer: Timer?) -> Void {
print("(Date()): 单次倒计时结束!")
}
还需要注意的是,oneshot1
中初始化得到 Timer 对象后,执行 RunLoop.main.add(timer:forMode:)
方法将定时器加入到 RunLoop 中,这一步是必须的,其中 forMode
参数我们使用默认模式(common
),可以防止其它高优先级 mode 的事件影响定时器的运行。打印信息时同时打印了当前时间,主要作为参考看倒计时对不对。下面我们测试一下我们定义的 oneshot1
方法:
TimerDemo.share.oneshot1()
可以看到打印结果,确实是 1 秒的时间间隔,且执行了定时器任务。
2019-03-05 13:51:07 +0000: Timer(timeInterval:repeats:block:) 初始化,单次
2019-03-05 13:51:08 +0000: 单次倒计时结束!
上面 oneshot1
和 oneFirehandler(timer:)
的实现可以简化为下面的形式,看您的使用习惯和需求决定使用哪种形式了:
func oneshot1() {
print("(Date()): Timer(timeInterval:repeats:block:) 初始化,单次")
timer = Timer(timeInterval: 1, repeats: false) { timer in
print("(Date()): 单次倒计时结束!")
}
RunLoop.main.add(timer!, forMode: .common)
}
我们继续试一下循环定时器,先定义一个循环定时器要处理的任务:
@objc private func loopFireHandler(_ timer: Timer?) -> Void {
if self.timeCount <= 0 {
timer!.invalidate()
print("(Date()): 循环倒计时结束!")
self.timeCount = 5
return
}
self.timeCount -= 1
print("(Date()): 倒计时 (self.timeCount) 秒")
}
loopFireHandler
方法中进行倒计时,到 0 时,调用定时器对象的 invalidate
方法释放定时器。另外定时器的实现如下 loop1
方法:
func loop1() {
print("(Date()): Timer(timeInterval:repeats:block:) 初始化,循环")
timer = Timer(timeInterval: 1, repeats: true, block: { timer in
self.loopFireHandler(timer)
})
RunLoop.main.add(timer!, forMode: .common)
}
可以看到只是 repeats
参数改为 true,block 中调用 loopFireHandler
方法,下面测试一下 loop1
:
TimerDemo.share.loop1()
运行结果如我们所想,实现了 5 秒倒计时:
2019-03-05 14:10:22 +0000: Timer(timeInterval:repeats:block:) 初始化,循环
2019-03-05 14:10:23 +0000: 倒计时 4 秒
2019-03-05 14:10:24 +0000: 倒计时 3 秒
2019-03-05 14:10:25 +0000: 倒计时 2 秒
2019-03-05 14:10:26 +0000: 倒计时 1 秒
2019-03-05 14:10:27 +0000: 倒计时 0 秒
2019-03-05 14:10:28 +0000: 循环倒计时结束!
2.1.1.2 Timer(timeInterval:target:selector:userInfo:repeats:)
类方法
- timeInterval 定时器时间间隔,单位为秒
- target 定时器每次循环结束后发送消息的目标对象
- selector 指定定时器每次计时结束要执行的方法,必须为
@objc
的函数对象,所以上面定义的oneFireHandler
和loopFireHandler
最前面要加@objc
关键词 - userinfo 指定用户信息,可以指定为 nil
- repeats 决定要不要循环执行,true 为循环,false 为单次
单次和循环的定时器处理,只有方法中的 repeats
参数和指定的处理任务函数不一样,其它都一致,所以后面只展示循环定时器的实现。
使用方法如下 loop2
方法:
// 使用 Timer(timeInterval:target:selector:userInfo:repeats:) 初始化对象,并将其添加到 main 线程的 RunLoop 中,循环
func loop2() {
print("(Date()): Timer(timeInterval:target:selector:userInfo:repeats:) 初始化,循环")
timer = Timer(timeInterval: 1, target: self, selector: #selector(self.loopFireHandler(_:)), userInfo: nil, repeats: true)
RunLoop.main.add(timer!, forMode: .common)
}
同样需要注意的是,需要将定时器 timer 添加到 RunLoop 中去。调用这个方法 TimerDemo.share.loop2()
即可看到打印结果:
2019-03-05 14:21:07 +0000: Timer(timeInterval:target:selector:userInfo:repeats:) 初始化,循环
2019-03-05 14:21:08 +0000: 倒计时 4 秒
2019-03-05 14:21:09 +0000: 倒计时 3 秒
2019-03-05 14:21:10 +0000: 倒计时 2 秒
2019-03-05 14:21:11 +0000: 倒计时 1 秒
2019-03-05 14:21:12 +0000: 倒计时 0 秒
2019-03-05 14:21:13 +0000: 循环倒计时结束!
2.1.2 第二种方式
2.1.2.1 Timer(fire:interval:repeats:block:)
类方法
这个方法相较于 Timer(timeInterval:repeats:block:)
只多了一个 fire
参数,这个参数代表的意思是什么时候触发第一次计时结束,类型是 Date 类,可以使用方法 Date(timeIntervalSinceNow:)
方法指定相对于现在的时刻,这个方法参数单位是秒,最终 Timer(fire:interval:repeats:block:)
类方法实现循环定时器的过程如下 loop3
:
// 使用 Timer(fire:interval:repeats:block:) 初始化对象,并将其添加到 main 线程的 RunLoop 中,循环
func loop3() {
print("(Date()): Timer(fire:interval:repeats:block:) 初始化,添加到 RunLoop,循环")
timer = Timer(fire: Date(timeIntervalSinceNow: 0), interval: 1, repeats: true, block: { timer in
self.loopFireHandler(timer)
})
RunLoop.main.add(timer!, forMode: .common)
}
注意 fire
参数,这里我们设置的是 Date(timeIntervalSinceNow: 0)
,也就是现在就触发一次计时结束,此时执行 TimerDemo.share.loop3()
可以看到结果:
2019-03-05 14:30:35 +0000: Timer(fire:interval:repeats:block:) 初始化,添加到 RunLoop,循环
2019-03-05 14:30:35 +0000: 倒计时 4 秒
2019-03-05 14:30:36 +0000: 倒计时 3 秒
2019-03-05 14:30:37 +0000: 倒计时 2 秒
2019-03-05 14:30:38 +0000: 倒计时 1 秒
2019-03-05 14:30:39 +0000: 倒计时 0 秒
2019-03-05 14:30:40 +0000: 循环倒计时结束!
可以看到第一行和第二行的打印时间都是 2019-03-05 14:30:35 +0000
这正匹配我们设置的 fire 参数,我们改一下 fire 参数为 Date(timeIntervalSinceNow: 3)
,也就是相对于现在延后 3 秒触发第一次计时结束,执行 TimerDemo.share.loop3()
看一下结果:
2019-03-05 14:33:00 +0000: Timer(fire:interval:repeats:block:) 初始化,添加到 RunLoop,循环
2019-03-05 14:33:03 +0000: 倒计时 4 秒
2019-03-05 14:33:04 +0000: 倒计时 3 秒
2019-03-05 14:33:05 +0000: 倒计时 2 秒
2019-03-05 14:33:06 +0000: 倒计时 1 秒
2019-03-05 14:33:07 +0000: 倒计时 0 秒
2019-03-05 14:33:08 +0000: 循环倒计时结束!
第二行与第一行的打印时间果然差了 3 秒,这下您应该明白这个 fire 参数的含义了吧!
2.1.2.2 Timer(fireAt:interval:target:selector:userInfo:repeats:)
类方法
这个方法相对于 Timer(timeInterval:target:selector:userInfo:repeats:)
方法多了一个 fireAt
参数,这个参数与上一个类方法的 fire
含义一致,使用就很简单了,定义 loop4
方法:
// 使用 Timer(fireAt:interval:target:selector:userInfo:repeats:) 初始化对象,并将其添加到 main 线程的 RunLoop 中,循环
func loop4() {
print("(Date()): Timer(fireAt:interval:target:selector:userInfo:repeats:) 初始化,添加到 RunLoop,循环")
timer = Timer(fireAt: Date(timeIntervalSinceNow: 0), interval: 1, target: self, selector: #selector(self.loopFireHandler(_:)), userInfo: nil, repeats: true)
RunLoop.main.add(timer!, forMode: .common)
}
这里就不展示执行结果了。
2.1.3 第三种方式
2.1.3.1 Timer.scheduledTimer(withTimeInterval:repeats:block:)
类方法
此方法参数与 Timer(timeInterval:repeats:block:)
参数一致,所以实现定时器一定难不倒我们了,但是这里需要注意的是,使用此方法得到的 timer 对象在执行方法的时候已经添加到 RunLoop 中了,所以不需要我们手动添加到 RunLoop 了:
// 使用 Timer.scheduledTimer(withTimeInterval:repeats:block:) 方法注册运行,循环
func loop5() {
print("(Date()): Timer.scheduledTimer(withTimeInterval:repeats:block:) 方法,循环")
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { timer in
self.loopFireHandler(timer)
})
}
2.1.3.2 Timer.scheduledTimer(timeInterval:target:selector:userInfo:repeats:)
类方法
此方法如同 Timer.scheduledTimer(withTimeInterval:repeats:block:)
方法,也不需要将方法返回的对象手动添加到 RunLoop 了:
// 使用 Timer.scheduledTimer(timeInterval:target:selector:userInfo:repeats:) 方法注册运行,循环
func loop6() {
print("(Date()): Timer.scheduledTimer(withTimeInterval:repeats:block:) 方法,循环")
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(self.loopFireHandler(_:)), userInfo: nil, repeats: true)
}
2.1.4 Timer 类定时器总结
综上,我们算是了解了 6 中类方法实现定时器,但是这种方式有个问题:因为是运行在 main 线程的的 RunLoop 中,所以可能出现时间延迟的问题,主线程主要处理 UI 控件的交互,如果在其中再有一些运算量大的操作,必定会影响定时器的执行(阻塞),也就是说 Timer 不论是在哪个 RunLoop 中,终究会是受 RunLoop 的影响。下面我们就再了解一个不受 RunLoop 影响的 GCD 方式的定时器。
2.2 GCD 方式的 Timer
通过 GCD 方式创建的定时器不会受 RunLoop 的影响,是一种比较底层的实现,所以很高效而且不受窗口控件频繁操作的影响。GCD 方式的定时器类型为 DispatchSourceTimer
,其实例对象可以使用 DispatchSource
的 MakeTimerSource
方法分配资源而得到,这个对象有以下常用方法:
- resume() 开启运行或恢复运行
- cancel() 取消定时器
- suspend() 挂起定时器,可以使用 resumne() 恢复运行
- setEventHandler(handler: (() -> Void)?) 设置定时器任务的处理
- setCancelHandler(handler: (() -> Void)?) 设置定时器取消时的处理
- schedule(deadline:repeating:leeway:) 配置定时器的时间参数
2.2.1 schedule(deadline:repeating:leeway:) 方法
这个是 DispatchSourceTimer
对象用来配置时间参数的方法,本节主要介绍一下其参数。
- deadline 类型为
DispatchTime
,这个时间精度可以达到纳秒级,含义与上面 Timer 类的Timer(fire:interval:repeats:block:)
方法的fire
一致,不过类型不一样。DispatchTime
有 now() 方法用来获取现在的时间,可以与DispatchTimeInterval
对象实现 + 操作,得到一个相对于时刻,DispatchTimeInterval
对象可以通过调用seconds(_:Int)
方法得到秒,milliseconds(_:Int)
方法得到毫秒,microseconds(_:Int)
方法得到微秒,nanoseconds(_:Int)
方法得到纳秒 - repeating 与 Timer 类方法中的 repeat 不是一个意思,这里代表的是定时器时间间隔也就是 timeInterval,类型为
DispatchTimeInterval
,比如指定一秒可以DispatchTimeInterval.seconds(1)
。这个参数具有默认值,当不指定这个参数的时候,这个参数就会设置为DispatchTimeInterval.never
,那么定时器只执行一次。 - leeway 时间容差,是一个定时时间的宽容度,具有一个默认值,所以调用此方法的时候可以不用指定这个参数,如果要指定的话,也是
DispatchTimeInterval
类型
2.2.2 使用方法
讲了上面这么多,还是用实际的例子说明,首先在 TimerDemo 类中添加如下属性:
private var gcdTimer: DispatchSourceTimer?
单次定时器,如方法 oneshot7
:
// GCD 方式的定时器,单次
func oneshot7() {
print("(Date()): GCD 方式的定时器,单次")
gcdTimer = DispatchSource.makeTimerSource()
gcdTimer?.setEventHandler() {
print("(Date()): 单次倒计时结束!")
}
gcdTimer?.schedule(deadline: .now() + .seconds(1))
gcdTimer?.resume()
}
循环定时器,如方法 loop7
:
// GCD 方式的定时器,循环
func loop7() {
print("(Date()): GCD 方式的定时器,循环")
gcdTimer = DispatchSource.makeTimerSource()
gcdTimer?.setEventHandler() {
if self.timeCount <= 0 {
self.gcdTimer?.cancel()
return
}
self.timeCount -= 1
print("(Date()): 倒计时 (self.timeCount) 秒")
}
gcdTimer?.setCancelHandler() {
print("(Date()): 倒计时结束!")
}
gcdTimer?.schedule(deadline: .now() + .seconds(1), repeating: .seconds(1))
gcdTimer?.resume()
}
注意:
如果定时器任务中需要对 UI 控件进行操作,要将那部分操作放在主线程进行也就是用下面的代码包裹:
DispatchQueue.main.async {
// 添加 UI 控件操作
}
2.2.3 总结
上述定时器演示的 playgroud 可以通过下面链接查看:
cocoaTimer.playgroundgist.github.com3. 两种方式示例对比
为了凸显 GCD 方式定时器不受 UI 的操作的影响,本小节新建一个名为 TimerDemo 的工程,工程的 Main.storyboard 中添加一个按钮,两个 label 和一个滑块,合理放置位置,比如:
![26399ba14913f6909b34625f1f13865f.png](https://i-blog.csdnimg.cn/blog_migrate/71ab4e04ebd1ab5fba9d5277944a6f3f.png)
分别将两个 label 的按钮绑定属性到 ViewController 类,同时为按钮添加一个 action 用来启动两种定时器。两个 label 分别显示 Timer 定时器的倒计时时间和 GCD 方式的定时器的倒计时时间,当启动定时器后,我们频繁滑动滑杆,观察两个定时器的情况,最终实现的 ViewController 类如下:
class ViewController: NSViewController {
@IBOutlet weak var timerLabel1: NSTextField!
@IBOutlet weak var timerLabel2: NSTextField!
@IBOutlet weak var timerButton: NSButton!
var count1 = 60
var count2 = 60
var timer: Timer?
var gcdTimer: DispatchSourceTimer?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
@IBAction func startTimer(_ sender: Any) {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
if self.count1 <= 0 {
timer.invalidate()
return
}
self.count1 -= 1
self.timerLabel1.stringValue = "timer: (self.count1) 秒"
}
gcdTimer = DispatchSource.makeTimerSource()
gcdTimer?.setEventHandler() {
if self.count2 <= 0 {
self.gcdTimer?.cancel()
return
}
self.count2 -= 1
DispatchQueue.main.async {
self.timerLabel2.stringValue = "gcdTimer: (self.count2) 秒"
}
}
gcdTimer?.schedule(deadline: .now() + .seconds(1), repeating: .seconds(1), leeway: .milliseconds(1))
gcdTimer?.resume()
self.timerButton.isEnabled = false
}
}
最后运行程序,疯狂滑动滑杆可以看到 GCD 方式的定时器丝毫不受影响,而 Timer 类的定时器受到了阻塞:
![61bde13c1f036ad302fe331829f84301.png](https://i-blog.csdnimg.cn/blog_migrate/e3a46451a72324226e6deb63eebd7e4c.jpeg)
示例工程下载:
TimerDemopichome-1254392422.cos.ap-chengdu.myqcloud.com博客原文:
MacOS 开发之定时器的使用www.smslit.top![2ff0c19a5f54dea98af7712338c986f0.png](https://i-blog.csdnimg.cn/blog_migrate/b85d519f93a7ef4c89f20ea30a2bae69.jpeg)