在程序开发中,我们常常借助定时器完成定时任务,比如短信验证码倒计时、运动计时等具有时间序列概念的操作。
最常用的定时方式有Timer 和GCD dispatchTimer
Timer的使用
Timer/NSTimer: 在某个时间间隔之后触发的定时器,向目标对象发送指定的消息。 Timer 的完成往往配合runloop
和相应的 mode。
初始化
创建即添加到当前 runloop
通过带 scheduledxxx
方法初始化的定时器,创建就会添加到当前 runloop ,且以 default mode的形式。即创建就开启。
比如以下初始化方法
scheduledTimer(timeInterval:invocation:repeats:)
scheduledTimer(timeInterval:target:selector:userInfo:repeats:)
scheduledTimer(timeInterval:repeats:block:)
创建后需要手动添加到 runloop
通过其他初始化方法创建的定时器,需要手动添加到 runloop(调用add(_:forMode)
),通过获取当前或者新建 runloop来添加。如果是新建 runloop 则需要手动开启 runloop 定时器才开启。
init(timeInterval:invocation:repeats:)
init(timeInterval:target:selector:userInfo:repeats:)
init(fire:interval:repeats:block)
init(fireAt:interval:target:selector:userInfo:repeats:)
通常我们是建议通过手动添加到 runloop 方式
其他方法和属性
// 触发定时器,相当于调一次,就会发送一次事件
open func fire()
// 启动时间设置,管理启动与停止
open var fireDate: Date
// 定时间隔
open var timeInterval: TimeInterval { get }
// 用来设置定时器误差的
@available(iOS 7.0, *)
open var tolerance: TimeInterval
// 使定时器失效,(重复性的定时一定记得释放对象之前或者使用完后设置)
open func invalidate()
// 检查定时器的有效性
open var isValid: Bool { get }
// 携带信息
open var userInfo: Any? { get }
fire()
方法用来马上触发一次定时效果,并不是用于开启定时器,定时器的开启决定于创建和 runloop 的运行。
Timer使用注意点
定时器启动
定时器不是主动启动的,要么创建就启动,要么创建后添加到 runloop 启动(或者通过添加后启动 runloop来启动)。
循环引用
由于 Timer 是一种资源,存在持有关系,一般情况下都会造成页面无法释放问题,关闭页面定时器依旧在运行。
而且这种循环引用使用 weak var timer
依然存在,也无法在 deinit()
方法中释放(原因循环引用不走)
解决方法主要有如下:
- [推荐] 使用闭包,且在闭包参数使用[weak self], iOS 10+
- 通过中间代理[weak self]实现Timer实现弱引用、iOS NSTimer 循环引用问题,注意自己通过临时 weak self 变量无法解决循环引用
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { [weak self](_) in // 使用 weak self
self?.printLog()
})
不准确问题
如果将 Timer 添加到默认主线程(即主 runloop)中, 如果主线程执行耗时操作或者切换到其他 mode(比如滑动 scrollView 切换到 TrackMode),此时定时器将不准确。尤其在 ScorllView 中使用 Timer要注意,至少使用 Commonmodes
将 timer 添加到其他手动创建的 runloop 中,且不会在这里进行耗时操作。
总结:不建议使用 Timer/NSTimer, 建议使用 GCD 定时器
应用进入后台会停止
一般,点击 Home后,1~3s App就会进入后台,此时就无法执行代码了。
实现短信验证倒计时
示例代码参考
1.创建一个重复定时器
countDownTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(timeFire), userInfo: nil, repeats: true)
2.实现定时器的Selector方法
func timeFire(){
if seconds >= 0 {//second:倒计时全局变量,初始值我设为60s
getVerCodeTitle = "\(seconds) S"
getVerCodeTimer.setTitle(getVerCodeTitle, for: .normal) //不能单纯设置titlelabel的text值,会一闪一闪的
getVerCodeTimer.isUserInteractionEnabled = false
seconds -= 1
}else{
getVerCodeTimer.isUserInteractionEnabled = true
getVerCodeTitle = "重发验证码"
getVerCodeTimer.setTitle(getVerCodeTitle, for: .normal)
countDownTimer.invalidate()//使定时器无效,
countDownTimer = nil //必须设置为空,否则计时器还是再走,不懂为什么,而且会导致第二次用的时候加速计时。
getVerCodeTimer.layer.borderColor = ASSIST_COLOR.cgColor
getVerCodeTimer.setTitleColor(ASSIST_COLOR, for: .normal)
// print("定时器状态\(countDownTimer.isValid)")
}
}
效果图:
点击前
点击后(倒计时)
使用GCD创建定时器(推荐)
在Swift4中GCD的语法更具有Swift形式,使用起来更方便了。
GCD创建的定时器,使得程序更安全,更准确,减少内存泄漏问题。
示例代码(定时重复性操作):
创建一个定时器
private var seconds = 59 // 倒计时
private var countDownTimer: DispatchSourceTimer = DispatchSource.makeTimerSource(flags: [], queue: DispatchQueue.main)
private var timerIsSuspend: Bool = false //避免取消暂停的定时器崩溃
let buttton = UIButton()
定时重复操作
func setupCountDown() {
let interval = 1
countDownTimer.schedule(deadline: .now(), repeating: .seconds(interval))
countDownTimer.setEventHandler { [weak self] in
guard let self = self else { return }
print("倒计时\(self.seconds)")
if self.seconds >= 0 {
let title = "重新发送" + "(\(self.seconds)s)"
self.buttton.setTitle(title, for: .normal)
self.buttton.isUserInteractionEnabled = false
self.seconds -= 1
} else {
self.buttton.isUserInteractionEnabled = true
self.buttton.setTitle(L10n.haAccount38, for: .normal)
self.countDownTimer.suspend() //暂停定时器
self.timerIsSuspend = true // 标记, 避免失败。
}
}
}
点击按钮,开启定时器
@objc
func tapSendEmailButton() {
seconds = 59 // 充值倒计时
countDownTimer.resume()
timerIsSuspend = false
}
页面销毁时会自定销毁定时器。
deinit {
if timerIsSuspend {
countDownTimer.resume()
}
countDownTimer.cancel()
}
常用操作
挂起定时器 timer.suspend() 通过resume 重启
唤醒: resume()
取消定时器: timer.cancel() //会销毁,无法重启
最容易崩溃的地方:
// 崩溃一:
gcdTimer.suspend()
gcdTimer = nil
// 崩溃二:
gcdTimer.suspend()
gcdTimer.cancel()
gcdTimer = nil
解决方案
先resume再cancel
注意: 如果定时器 处于挂起状态,cancel会奔溃; 定时器挂起=唤醒+1【原理参考】