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), 可以直接关闭改计时器.

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容