概述
本文将要讨论以下三种timer:
- NSTimer (Timer)
- CADisplayLink
- GCD
原理
计时好比数数, 在iOS中, 数数的人是系统内核.
内核会根据一些设定好的条件 (比如按时) 产生相应事件, 然后通过回调函数向外抛出 (可理解为"报时"). 我们通过注册观察者来监听取得这些回调, 从而达到计时的目的.
这些与时间相关的事件的载体叫做事件源 (Source), iOS中有两种 Source: Run Loop Source
和Dispatch Source
.
-
Run Loop Source
会唤醒当前Run Loop, 然后执行回调函数.
关于Run Loop Source, 可参考之前的博文iOS开发之进阶篇(8)—— Run Loops. -
Dispatch Source
也会产生一些特定的事件, 事件通过 block 自动加入到对应的 dispatch queue 中.
关于Dispatch Source, 可参考苹果文档.
也就是说, iOS中有两种计时方式, 分别对应Run Loop
和Dispatch (GCD)
中的两种计时器源.
- NSTimer (Timer) 和 CADisplayLink 都属于
Run Loop Source
. - GCD 属于
Dispatch Source
.
以上是口语解说, 如果你在等待书面文解释, 抱歉, 木有. 😂 😂
精确度
- NSTimer (Timer)
Run Loop
中的计时器源会受到模式的影响, 如果模式不对则不会触发. 比如说添加到主线程中的NSTimer, 当滚动 Scroll View 的时候, 模式发生改变, NSTimer暂时失效.
而重复计时器会受到当前事务的影响, 可能在一定范围内产生偏差, 偏差大小则和计时器的tolerance属性有关.
如果触发时间延迟得太久, 以致错过了一个或多个计划的触发时间, 则 timer 将在错过的时间段内仅触发一次.
- CADisplayLink
CADisplaylink 也是由Run Loop
中的计时器源触发, 它与NSTimer相似, 都可以以一定的时间间隔触发回调 selector. 不同的是, CADisplaylink 的时间间隔是与屏幕的刷新频率相关联的.
CADisplayLink 的时间间隔由preferredFramesPerSecond属性来决定的, 意为每秒刷新的帧数. 如果设备的最大刷新率是每秒60帧, 则实际帧速率包括每秒15、20、30和60帧.
如果你设置为一个特定的值, 他么这个值最终也会和15、20、30以及60之间做最合适匹配. 比如设置为26或35, 则实际帧速率为30.
iOS设备的屏幕刷新频率是固定的, 因此CADisplayLink在正常情况下会在每次刷新结束都被调用, 精确度相当高. 但不要在CADisplayLink计时器中做耗时操作, 因为可能会被系统忽略掉.
- GCD
由于GCD拥有强大的资源配置能力, 因此其计时器精确度是相当可观的. 而且GCD计时器不需添加到run loop中, 因此在子线程中也可直接使用.
使用GCD定时并不是说在指定时间后马上执行任务, 而是在指定时间后将任务添加到队列. 任务的执行则取决于当前的队列状态(串并行, 是否挂起等). 比如下面例子, 定时看似应该在2秒后执行, 结果却是5秒后:
NSLog(@"start");
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0*NSEC_PER_SEC)), queue, ^{
NSLog(@"time is up");
});
NSLog(@"end");
sleep(5);
// ------------------------------------------------------
log:
2020-08-11 14:43:41.959636+0800 KKTimerDemo[2119:91835] start
2020-08-11 14:43:41.959804+0800 KKTimerDemo[2119:91835] end
2020-08-11 14:43:46.978124+0800 KKTimerDemo[2119:91835] time is up
1. NSTimer (Timer)
OC:
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic, strong) NSTimer *mainTimer;
@property (nonatomic, strong) NSTimer *globalTimer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 在主线程中创建NSTimer
[self createTimerInMain];
// 在子线程中创建NSTimer
[self createTimerInGlobal];
}
- (void)dealloc
{
// 销毁定时器
[self cancelMainTimer];
[self cancelGlobalTimer];
}
#pragma mark - 创建
// 在主线程中创建NSTimer
- (void)createTimerInMain {
self.mainTimer = [NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:@selector(mainTimeIsUp:) userInfo:nil repeats:YES];
// [self.mainTimer fire]; // 立即执行
}
// 在子线程中创建NSTimer
- (void)createTimerInGlobal {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 法一
self.globalTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(globalTimeIsUp:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] run];
// 法二
// self.globalTimer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(globalTimeIsUp:) userInfo:nil repeats:YES];
// [[NSRunLoop currentRunLoop] addTimer:self.globalTimer forMode:NSRunLoopCommonModes]; // Common Mode 不受 Scroll View 滚动影响
// [[NSRunLoop currentRunLoop] run];
});
}
#pragma mark - 销毁
// 取消 mainTimer
- (void)cancelMainTimer {
if (_mainTimer) {
[self.mainTimer invalidate];
self.mainTimer = nil;
}
}
// 取消 globalTimer
- (void)cancelGlobalTimer {
if (_globalTimer) {
[self.globalTimer invalidate];
self.globalTimer = nil;
}
}
#pragma mark - 定时时间到
// mainTimer 定时时间到
- (void)mainTimeIsUp:(NSTimer *)timer {
NSLog(@"mainTimeIsUp");
}
// globalTimer 定时时间到
- (void)globalTimeIsUp:(NSTimer *)timer {
NSLog(@"globalTimeIsUp");
}
@end
Swift:
import Foundation
class SwiftTimer: NSObject {
var mainTimer: Timer?
var globalTimer: Timer?
// MARK: - 创建
// 在主线程中创建NSTimer
@objc func createMainTimer() {
mainTimer = Timer.scheduledTimer(timeInterval: 5.0, target: self, selector: #selector(mainTimeIsUp(timer:)), userInfo: nil, repeats: true)
// mainTimer?.fire() // 立即执行
}
// 在子线程中创建NSTimer
@objc func createGlobalTimer() {
DispatchQueue.global().async {
// 法一
self.globalTimer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(self.globalTimeIsUp(timer:)), userInfo: nil, repeats: true)
RunLoop.current.run();
// 法二
// self.globalTimer = Timer.init(timeInterval: 1.0, target: self, selector: #selector(self.globalTimeIsUp(timer:)), userInfo: nil, repeats: true)
// RunLoop.current.add(self.globalTimer!, forMode: .common)
// RunLoop.current.run();
}
}
// MARK: - 销毁
@objc func cancelMainTimer() {
self.mainTimer?.invalidate()
self.mainTimer = nil
}
@objc func cancelGlobalTimer() {
self.globalTimer?.invalidate()
self.globalTimer = nil
}
// MARK: - 定时时间到
@objc func mainTimeIsUp(timer: Timer) {
print("mainTimeIsUp")
}
@objc func globalTimeIsUp(timer: Timer) {
print("globalTimeIsUp")
}
}
2. CADisplayLink
OC:
#pragma mark - CADisplayLink
- (void)createTimerUseCADisplayLink {
self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLick)];
self.link.preferredFramesPerSecond = 60;
[self.link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)cancelTimer {
if (_link) {
[self.link invalidate];
self.link = nil;
}
}
- (void)displayLick {
NSLog(@"displayLick");
}
Swift:
import QuartzCore
// MARK - CADisplayLink
@objc func createTimerUseCADisplayLink() {
self.link = CADisplayLink.init(target: self, selector: #selector(displayLick))
self.link?.preferredFramesPerSecond = 60
self.link?.add(to: RunLoop.current, forMode: .default)
}
@objc func calcelTimer() {
self.link?.invalidate()
self.link = nil
}
@objc func displayLick() {
print("displayLick")
}
3. GCD
OC:
@property (nonatomic, strong) dispatch_source_t gcdTimer;
- (void)createGCDTimer {
// 定时一次
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"dispatch_after");
});
// 循环
self.gcdTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
// 1:计时器源 2:开始时间 3:间隔 4:误差
dispatch_source_set_timer(self.gcdTimer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(self.gcdTimer, ^{
// do something
NSLog(@"dispatch_event_handler");
});
dispatch_resume(self.gcdTimer); // 开启定时器, 立即执行 dispatch_source_set_event_handler 里任务
}
- (void)cancelGCDTimer {
if(_gcdTimer){
dispatch_source_cancel(self.gcdTimer);
self.gcdTimer = nil;
}
}
注: 如需取消
dispatch_after
, 使用dispatch_block_cancel
Swift:
// MARK - GCD
static var gcdTimer: DispatchSourceTimer?
@objc func createGCDTimer() {
// 执行一次
DispatchQueue.main.asyncAfter(deadline: .now()+1.0) {
print("asyncAfter")
}
// 循环
SwiftTimer.gcdTimer = DispatchSource.makeTimerSource(flags: [], queue: DispatchQueue.global())
// SwiftTimer.gcdTimer = DispatchSource.makeTimerSource() // 默认主线程
SwiftTimer.gcdTimer?.schedule(deadline: .now(), repeating: .seconds(1), leeway: .nanoseconds(0))
SwiftTimer.gcdTimer?.setEventHandler {
print("dispatch_event_handler")
}
SwiftTimer.gcdTimer?.resume() // 开始, 立即执行
}
@objc func cancelGCDTimer() {
SwiftTimer.gcdTimer?.cancel()
SwiftTimer.gcdTimer = nil
}
注意:
因为本例是用OC调用Swift, 所以gcdTimer用static修饰, 使其执行过程中不会被释放. 如果在Swift工程中使用, 使其定义为全局变量即可, 不需static修饰.