说起计时器,很多开发人员第一时间就会想起Timer,但是随着使用的深入,慢慢就发现Timer不是很精确,随后就有想到GCD Timer,专业点就是DispatchSourceTimer,除了这两个还有一个,那就是CADisplayLink,没错,这些都可以用于定时器使用。
本篇文章就对这三种定时器进行阐述,讲解其用法、注意事项,以及利弊。
首先强调一下,本篇文章的所有API以及代码全部基于Swift语言。
文中如果不对的地方,还请指正!
Timer,我们先看看官方的定义:
A timer that fires after a certain time interval has elapsed, sending a specified message to a target object.
一个计时器,在经过一定的时间间隔后触发,向目标对象发送指定的消息。
之前的文章(iOS Runloop探索(快速了解Runloop))中讲到过,Timer与Runloop是息息相关的,没有Runloop,Timer根本无法运行,创建一个Timer对象后,需要显式或隐式的将其加入到指定Runloop的mode里面,然后等待Runloop循环调用。
另外,Timer不是采用实时机制,
下面列表中的方法都是Timer的初始化方法,
上表中,前三个类方法在创建完Timer对象后,会将其添加到当前的Runloop的默认Mode里面,而后面的几个init方法则需要手动调用add(_:forMode:)
方法将其加入到指定的Runloop的Mode里面。
Timer的运行离不开线程,下面将就主线程和子线程中的Timer进行讲解。
主线程在程序启动的时候就已经开启了,与主线程关联的Runloop也已经开启了,所以说在主线程中用Timer还是比较容易的,但是主线程毕竟任务繁多,可能会影响到Timer的执行效率。
override func viewDidLoad() {
super.viewDidLoad()
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { (timer) in
print("我默认已经加入到Runloop中了")
}
let timer2 = Timer(timeInterval: 1, repeats: true) { (timer) in
print("我需要手动加入到Runloop中")
}
RunLoop.current.add(timer2, forMode: .default)
}
上面的两种初始化方法是没问题的,timer都能正常的运行。如果将这段代码放到某个二级界面(由一级界面push或者present过来的),那么就有问题了,有什么问题呢?看一下输出结果:
我默认已经加入到Runloop中了
我需要手动加入到Runloop中
我默认已经加入到Runloop中了
我需要手动加入到Runloop中
deinit
我默认已经加入到Runloop中了
我需要手动加入到Runloop中
我默认已经加入到Runloop中了
我需要手动加入到Runloop中
二级界面都deinit了,timer还在运行,是谁持有了这两个timer呢?苹果官方有这样一段话:
Timers work in conjunction with run loops. Run loops maintain strong references to their timers, so you don’t have to maintain your own strong reference to a timer after you have added it to a run loop.
意思就是Runloop会强引用加入到其中的Timer,所以我们需要在二级界面销毁的时候干掉Runloop中的timer,代码改为:
var timer1: Timer?
var timer2: Timer?
deinit {
print("deinit")
timer1?.invalidate()
timer2?.invalidate()
}
override func viewDidLoad() {
super.viewDidLoad()
timer1 = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { (timer) in
print("我默认已经加入到Runloop中了")
}
timer2 = Timer(timeInterval: 1, repeats: true) { (timer) in
print("我需要手动加入到Runloop中")
}
RunLoop.current.add(timer2!, forMode: .default)
}
invalidate()方法将会从Runloop中移除Timer,并停止timer的运行。
子线程中的Runloop默认是不开启的,而且在子线程中Runloop是懒加载的,只有第一次获取的时候才会创建。
override func viewDidLoad() {
super.viewDidLoad()
DispatchQueue.global().async {
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { (timer) in
print("我默认已经加入到Runloop中了")
}
let timer2 = Timer(timeInterval: 1, repeats: true) { (timer) in
print("我需要手动加入到Runloop中")
}
RunLoop.current.add(timer2, forMode: .default)
// 子线程中的Runloop默认是没有开启的,需要调用run()方法开启。
RunLoop.current.run()
}
}
同样的,如果在二级界面执行上面代码,同样会出现问题的,界面销毁了,而Timer还在运行,原理同上。
在讲Timer加入到Runloop的时候,需要额外注意指定的Mode,有的界面设计到滚动或者一些控件的拖动,此时有可能会导致Timer停止运行。
名称 | 描述 |
---|---|
static let common: RunLoop.Mode | 使用该值作为模式被添加到Runloop的对象,将会被Runloop的所有的模式监控,也就是该模式包含了default和tracking两个模式。 |
static let `default`: RunLoop.Mode | 处理输入源的模式,Runloop常用的模式,App的默认Mode,通常主线程是在这个Mode下运行。 |
static let tracking: RunLoop.Mode | 在跟踪控件的时候使用该模式。界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。 |
比如界面有ScrollView,如果将Timer添加到default模式下,那么在界面滚动的时候,Timer将会停止。
如果将Timer添加到tracking模式下,那么只有在界面滚动的时候,Timer才会运行,滚动停止,Timer停止。
开发中,除了特殊的需求,一般都将Timer添加到common模式下,以保证Timer正常运行。
循环引用,你持有我,我持有你,谁也不放了谁,谁也都释放不了。
Timer如果用的不小心,很可能就导致了循环引用。
var timer1: Timer?
var timer2: Timer?
deinit {
print("deinit")
timer1?.invalidate()
timer2?.invalidate()
}
override func viewDidLoad() {
super.viewDidLoad()
timer1 = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] (timer) in
self?.saySomething1()
}
timer2 = Timer(timeInterval: 1, repeats: true) { [weak self] (timer) in
self?.saySomething2()
}
RunLoop.current.add(timer2!, forMode: .default)
}
func saySomething1() {
print("我默认已经加入到Runloop中了")
}
func saySomething2() {
print("我需要手动加入到Runloop中")
}
上面代码中,如果没有在Timer的闭包中加上[weak self],那么肯定会发生循环引用的。
DispatchSourceTimer,也就是大家通常叫的GCD Timer,是依赖于GCD的一种Timer,Runloop的底层代码中也用到这种Timer,可见GCD Timer并不依赖与Runloop。
先看一下苹果的定义:
A dispatch source that submits the event handler block based on a timer.
使用下面的方法即可创建一个DispatchSourceTimer对象。
class func makeTimerSource(flags: DispatchSource.TimerFlags = [], queue: DispatchQueue? = nil) -> DispatchSourceTimer
// 默认在主队列中调度使用
let timer = DispatchSource.makeTimerSource()
// 指定在主队列中调度使用
let timer = DispatchSource.makeTimerSource(flags: [], queue: DispatchQueue.main)
// 指定在全局队列中调度使用
let timer = DispatchSource.makeTimerSource(flags: [], queue: DispatchQueue.global())
// 指定在自定义队列中调度使用
let customQueue = DispatchQueue(label: "customQueue")
let timer = DispatchSource.makeTimerSource(flags: [], queue: customQueue)
配置Timer参数,需要使用DispatchSourceTimer协议的方法。可以安排一次或多次触发的Timer。Timer每次触发的时候,都会调用已部署的任务。
Methods | Description |
---|---|
func schedule(deadline: DispatchTime, repeating: DispatchTimeInterval, leeway: DispatchTimeInterval) | Schedules a timer with the specified deadline, repeat interval, and leeway values. |
func schedule(deadline: DispatchTime, repeating: Double, leeway: DispatchTimeInterval) | Schedules a timer with the specified deadline, repeat interval, and leeway values. |
func schedule(wallDeadline: DispatchWallTime, repeating: DispatchTimeInterval, leeway: DispatchTimeInterval) | Schedules a timer with the specified time, repeat interval, and leeway values. |
func schedule(wallDeadline: DispatchWallTime, repeating: Double, leeway: DispatchTimeInterval) | Schedules a timer with the specified time, repeat interval, and leeway values. |
调用上面的方法可配置Timer的开始时间,重复执行时间,以及允许的偏差量。
// 从现在开始,每秒执行一次。
timer?.schedule(deadline: DispatchTime.now(), repeating: .seconds(1), leeway: .nanoseconds(1))
// 5秒之后执行任务,不重复。
timer?.schedule(deadline: DispatchTime.now() + 5, repeating: .never, leeway: .nanoseconds(1))
当Timer配置完参数后,使用DispatchSourceProtocol协议的方法来部署要执行的任务。
Methods | Description |
---|---|
func setEventHandler(handler: DispatchWorkItem) | Sets the event handler work item for the dispatch source. |
func setEventHandler(qos: DispatchQoS, flags: DispatchWorkItemFlags, handler: Self.DispatchSourceHandler?) | Sets the event handler work item for the dispatch source. |
func setRegistrationHandler(handler: DispatchWorkItem) | Sets the registration handler work item for the dispatch source. |
func setRegistrationHandler(qos: DispatchQoS, flags: DispatchWorkItemFlags, handler: Self.DispatchSourceHandler?) | Sets the registration handler work item for the dispatch source. |
func setCancelHandler(handler: DispatchWorkItem) | Sets the cancellation handler block for the dispatch source. |
func setCancelHandler(qos: DispatchQoS, flags: DispatchWorkItemFlags, handler: Self.DispatchSourceHandler?) | Sets the cancellation handler block for the dispatch source with the specified quality-of-service class and work item options. |
上表是苹果官方的描述,下面看一下setEventHandler和setRegistrationHandler的区别:
例如下面的代码:
var timer: DispatchSourceTimer?
func initTimer() {
// 默认在主队列中调度使用
timer = DispatchSource.makeTimerSource()
// 从现在开始,每秒执行一次。
timer?.schedule(deadline: DispatchTime.now(), repeating: .seconds(1), leeway: .nanoseconds(1))
// 5秒之后执行任务,不重复。
// timer?.schedule(deadline: DispatchTime.now() + 5, repeating: .never, leeway: .nanoseconds(1))
timer?.setEventHandler {
DispatchQueue.main.async {
print("执行任务")
}
}
timer?.setRegistrationHandler(handler: {
DispatchQueue.main.async {
print("Timer开始工作了")
}
})
timer?.activate()
}
执行结果如下:
2020-11-28 02:20:00 +0000 Timer开始工作了
2020-11-28 02:20:00 +0000 执行任务
2020-11-28 02:20:01 +0000 执行任务
2020-11-28 02:20:02 +0000 执行任务
下面看一下Timer的一下控制方法及状态:
Methods | Description |
---|---|
func activate() | Activates the dispatch source. |
func suspend() | Suspends the dispatch source. |
func resume() | Resumes the dispatch source |
func cancel() | Asynchronously cancels the dispatch source, preventing any further invocation of its event handler block. |
var isCancelled: Bool | Returns a Boolean indicating whether the given dispatch source has been canceled. |
方法详解:
上面的这些方法如果使用不当,很容易造成APP崩溃,下面来看一下具体注意事项及建议:
A timer object that allows your application to synchronize its drawing to the refresh rate of the display.
CADisplayLink是一个定时器,允许你的应用以和屏幕刷新率相同的频率将内容显示到屏幕上。
在应用中创建一个新的 CADisplayLink 对象,并给它提供一个 target
和selector
,当屏幕刷新的时候会调用。为了同步显示内容,需要使用add(to:forMode:)
方法把它添加到runloop
中。
一但 CADisplayLink
以指定的模式添加到Runloop
之后,每当屏幕需要刷新的时候,Runloop
就会调用CADisplayLink
绑定的target
上的selector
,这时target
可以访问到 CADisplayLink
的每次调用的时间戳,该时间戳是上一帧显示的时间,这样可以用来准备下一帧显示需要的数据。
Methods | Description |
---|---|
init(target: Any, selector: Selector) | display link的构造方法,需要指定target和selector。selector方法中可带displaylink参数,方法示例如下: func selector(displaylink: CADisplayLink) |
func add(to: RunLoop, forMode: RunLoop.Mode) | 将display link加入指定的Runloop的Mode里。 |
func remove(from: RunLoop, forMode: RunLoop.Mode) | 将display link从指定的Runloop的Mode里移除。 |
func invalidate() | 将display link从Runloop的所有Mode中移除,同时释放所持有的target。invalidate()方法是线程安全的,因此允许在其他线程停止正在运行的display link。 |
简单的创建方法如下:
func createDisplayLink() {
let displaylink = CADisplayLink(target: self, selector: #selector(updateAction))
displaylink.add(to: .current, forMode: .default)
}
@objc func updateAction(displaylink: CADisplayLink) {
print(displaylink.timestamp)
}
Properties | Description |
---|---|
var duration: CFTimeInterval | 屏幕刷新时间间隔,只读属性。只有当Selector方法执行过一次才会有值。如果iOS设备的刷新频率是60HZ,那么屏幕16.7ms刷新一次,即duration为16.7ms。这个值取决于设备的刷新频率,会有波动。 |
var preferredFramesPerSecond: Int | 指定的Selector的调用频率。在iOS上,每秒的最大帧数通常是60,所以其最大值为60。默认值是0,如果设置为0,意味着Selector的调用频率和屏幕的最大刷新频率相同。 |
var isPaused: Bool | 控制计时器暂停与恢复。 |
var timestamp: CFTimeInterval | 屏幕显示的上一帧的时间戳,这个属性通常被target用来计算下一帧中应该显示的内容。 |
var targetTimestamp: CFTimeInterval | 屏幕下一帧显示时的时间戳,可以使用targetTimestamp取消或暂停可能超出帧间可用时间的长时间运算等,以保持一致的帧速率。 |
针对targetTimestamp的用法,苹果给出了一个示例,如下:
下面的的step方法中,做了一个耗时的运算,每次循环的时候,判断一下当前的时间是否超过了下一帧显示的时间,如果超过了,退出循环。
func createDisplayLink() {
let displayLink = CADisplayLink(target: self, selector: #selector(step))
displayLink.add(to: .main, forMode: .defaultRunLoopMode)
}
func step(displayLink: CADisplayLink) {
var sqrtSum = 0.0
for i in 0 ..< Int.max {
sqrtSum += sqrt(Double(i))
if (CACurrentMediaTime() >= displayLink.targetTimestamp) {
print("break at i =", i)
break
}
}
}
优点:
缺点:
本篇文章主要讲解了iOS开发常用的三个计时器,Timer、DispatchSourceTimer、CADisplayLink,每个计时器都有各自的原理以及优缺点,使用的时候应该根据具体情况而选择。
三者不同之处:
。
Timer
以指定的模式注册到runloop后,每当设定的周期时间到达后,Runloop会向指定的target发送一次指定的selector消息。preferredFramesPerSecond属性设置每次调用回调方法的次数。
本篇文章出自https://blog.csdn.net/guoyongming925的博客,如需转载,请标明出处。