iOS -- 常用计时器(Timer, DispatchSourceTimer, CADisplayLink)

项目中常见的功能: 验证码倒计时提示, 广告页3秒倒计时等, 实现这些功能自然就要用到计时器了, 在此记录一下这些计时器的基础使用以及注意事项.

注: 基于swift 4.0

Timer

Timer有多种初始化方法, 分为两大类: 实例方法 & 类方法

实例方法:

第一种: block类型
- parameter: timeInterval 时间间隔, 重复操作中代表间隔多久执行一次;
- parameter: repeats 是否重复, 如果为true, 默认间隔timeInterval时间执行一次;
- parameter: block 计时器的执行主体, 需要执行的操作;
timer = Timer(timeInterval: 1.0, repeats: true, block: { (time) in
            // 需要执行的操作
        })
第二种: target + selector类型
- parameter: timeInterval 时间间隔, 重复操作中代表间隔多久执行一次;
- parameter: target "selector"的执行者;
- parameter: selector 计时器触发的Action, 需要执行的操作;
- parameter: userInfo 想通过timer传递的数据;
- parameter: repeats 是否重复, 如果为true, 默认间隔timeInterval时间执行一次;
// 初始化timer
timer = Timer(timeInterval: 1, target: self, selector: #selector(timerAction), userInfo: nil, repeats: true)

// 执行的Action
@objc private func timerAction() {
        // 需要执行的操作
    }
第三种: 指定时间开启计时器
- parameter: fire 设置指定时间, 计时器将在该时间开启;
- parameter: interval 时间间隔, 重复操作中代表间隔多久执行一次;
- parameter: repeats 是否重复, 如果为true, 默认间隔timeInterval时间执行一次;
- parameter: block 计时器的执行主体, 需要做的操作放到里面;
timer = Timer(fire: Date(timeIntervalSinceNow: 0), interval: 1, repeats: true, block: { (time) in
            // 需要执行的操作
        })
- parameter: fireAt 设置指定时间, 计时器将在该时间开启;
- parameter: interval 时间间隔, 重复操作中代表间隔多久执行一次;
- parameter: target "selector"的执行者;
- parameter: selector 计时器触发的Action, 需要执行的操作;
- parameter: userInfo 想通过timer传递的数据;
- parameter: repeats 是否重复, 如果为true, 默认间隔timeInterval时间执行一次;
// 初始化
timer = Timer(fireAt: Date(timeIntervalSinceNow: 0), interval: 1, target: self, selector: #selector(timerAction), userInfo: nil, repeats: true)

// 执行的Action
@objc private func timerAction() {
        // 需要执行的操作
    }
注意事项:
1.通过实例方法初始化的计时器, 必须手动加入RunLoop, 否则不会被开启;
RunLoop.current.add(timer!, forMode: .defaultRunLoopMode)
2.如果计时器不重复执行, 只执行一次, 除了上面的方式, 还可以用下面这种方式, 但是下面这种方式只适合单次的情况, 如果是重复执行, 则无效
timer?.fire()

类方法

第一种: block类型
- parameter: withTimeInterval 时间间隔, 重复操作中代表间隔多久执行一次;
- parameter: repeats 是否重复, 如果为true, 默认间隔timeInterval时间执行一次;
- parameter: block 计时器的执行主体, 需要执行的操作;
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { (time) in
            // 需要执行的操作
        })
第二种: target + selector类型
- parameter: timeInterval 时间间隔, 重复操作中代表间隔多久执行一次;
- parameter: target "selector"的执行者;
- parameter: selector 计时器触发的Action, 需要执行的操作;
- parameter: userInfo 想通过计时器传递的数据;
- parameter: repeats 是否重复, 如果为true, 默认间隔timeInterval时间执行一次;
// 初始化
timer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(timerAction), userInfo: nil, repeats: true)

// 执行的Action
@objc private func timerAction() {
        // 需要执行的操作
    }
注意事项:
1. 通过类方法初始化的timer无需手动加入RunLoop, 会自动被加入RunLoop.main的defaultRunLoopMode.
注: 其实初始化方法还有以下两个, 因为涉及到OC里的NSInvocation, 这里就不展开讲解了, 感兴趣的可以看一下NSInvocation详解.
// 实例方法
timer = Timer(timeInterval: 1, invocation: NSInvocation实例对象, repeats: true)
// 类方法
timer = Timer.scheduledTimer(timeInterval: 1, invocation: NSInvocation实例对象, repeats: true)

关闭timer

timer?.invalidate()
timer = nil

Timer总结:

1. 无论通过哪种方式初始化的计时器(除了通过NSInvocation初始化的会被立即执行), 都不会立即执行, 一般都是经过一个时间间隔timeInterval的值才开始执行.
2. 存在延时性, 不管是一次性的还是周期性的timer的实际触发事件的时间,都会与所加入的RunLoop和RunLoop Mode有关,如果此RunLoop正在执行一个连续性的运算,timer就会被延时出发。重复性的timer遇到这种情况,如果延迟超过了一个周期,则会在延时结束后立刻执行,并按照之前指定的周期继续执行。
3. 通过实例方法初始化的timer, 必须手动加入RunLoop.

DispatchSourceTimer

间隔定时器, 相当于repeats设置为true的Timer.

初始化

// 方式一: 直接使用默认值初始化
gcdTimer = DispatchSource.makeTimerSource()

// 方式二: flag(标记) + queue(队列)
gcdTimer = DispatchSource.makeTimerSource(flags: [], queue: DispatchQueue.global())

设置timer参数

- parameter: deadline 截止时间, 计时器最迟开始时间;
- parameter: wallDeadline 截止时间, 计时器最迟开始时间;
- parameter: repeating 时间间隔;
- parameter: leeway 容忍时间;
// 通过设置deadline
gcdTimer?.schedule(deadline: DispatchTime.now(), repeating: DispatchTimeInterval.seconds(1), leeway: DispatchTimeInterval.seconds(0))

// 通过设置wallDeadline
gcdTimer?.schedule(wallDeadline: DispatchWallTime.now(), repeating: DispatchTimeInterval.seconds(1), leeway: DispatchTimeInterval.seconds(0))
注意事项:
1. 参数leeway, 指的是一个期望的容忍时间,将它设置为1秒,意味着系统有可能在定时器时间到达的前1秒或者后1秒才真正触发定时器。在调用时推荐设置一个合理的 leeway 值。需要注意,就算指定 leeway 值为 0,系统也无法保证完全精确的触发时间,只是会尽可能满足这个需求。
2. 关于deadline和wallDeadline, 大部分文章中的说法是这样的: 使用 deadline, 系统会使用默认时钟来进行计时, 然而当系统休眠的时候, 默认时钟是不走的, 也就会导致计时器停止; 而使用 wallDeadline可以让计时器按照真实时间间隔进行计时; 但是经过反复测试, 并没有体现出两者的区别, 所以如果知道的同学, 欢迎留言交流.

设置timer事件

gcdTimer?.setEventHandler(handler: {
            // 需要执行的操作
        })
开启计时器 & 暂定计时器 & 关闭计时器
// 开启计时器
gcdTimer?.resume()

// 暂停计时器
gcdTimer?.suspend()
// 暂停后重启计时器
gcdTimer?.resume()

// 关闭计时器
gcdTimer?.cancel()
gcdTimer = nil
示例: 获取验证码60s倒计时
var total = 60
gcdTimer = DispatchSource.makeTimerSource()
gcdTimer?.schedule(wallDeadline: DispatchWallTime.now(), repeating: DispatchTimeInterval.seconds(1), leeway: DispatchTimeInterval.seconds(0))
gcdTimer?.setEventHandler(handler: { [weak self] in
            if total <= 0 {
                self?.gcdTimer?.cancel()
                self?.gcdTimer = nil
            } else {
                DispatchQueue.main.async {
                    self?.DispatchSourceTimerBtn.setTitle("\(total)s", for: .normal)
                    total -= 1
                }
            }
        })
gcdTimer?.resume()

注意事项: 下面两种操作会造成程序崩溃, 原因是: gcdTimer执行了suspend()操作后, 是不可以被直接释放的, 如果想关闭一个执行了suspend()操作的计时器, 需要先执行resume(), 再执行cancel(), 最后置nil.

// 崩溃一:
gcdTimer?.suspend()
gcdTimer = nil

// 崩溃二: 
gcdTimer?.suspend()
gcdTimer?.cancel()
gcdTimer = nil

CADisplayLink

屏幕刷新时调用:CADisplayLink是一个能让我们以和屏幕刷新率同步的频率将特定的内容画到屏幕上的定时器类. CADisplayLink以特定模式注册到runloop后, 每当屏幕显示内容刷新结束的时候, runloop就会向CADisplayLink指定的target发送一次指定的selector消息, CADisplayLink类对应的selector就会被调用一次. 所以通常情况下, 按照iOS设备屏幕的刷新率60次/秒, CADisplayLink默认每秒运行60次, 但是通过它的preferredFramesPerSecond属性可以改变每秒运行帧数,如设置为2, 意味CADisplayLink每秒运行2次.
延迟:iOS设备的屏幕刷新频率是固定的,CADisplayLink在正常情况下会在每次刷新结束都被调用,精确度相当高。但如果调用的方法比较耗时导致CPU过于繁忙,超过了屏幕刷新周期,就会导致跳过若干次回调调用机会, 跳过次数取决CPU的忙碌程度.
使用场景:从原理上可以看出,CADisplayLink适合做界面的不停重绘,比如视频播放的时候需要不停地获取下一帧用于界面渲染。

初始化

- parameter: target "selector"的执行者;
- parameter: selector 计时器触发的Action, 需要执行的操作;
cadTimer = CADisplayLink(target: self, selector: #selector(cadTimerAction))

设置

// 修改为每秒执行2次
cadTimer?.preferredFramesPerSecond = 2
// 添加到RunLoop
cadTimer?.add(to: RunLoop.current, forMode: .defaultRunLoopMode)

// 暂停
cadTimer?.isPaused = true
// 继续
cadTimer?.isPaused = false

// 关闭
cadTimer?.invalidate()
cadTimer = nil

注意事项: DispatchSourceTimer处于暂停状态下不可以直接关闭, 而CADisplayLink与DispatchSourceTimer不同, 如果一个CADisplayLink对象处于暂停状态(isPaused = true), 可以直接关闭改计时器.

推荐阅读更多精彩内容