通过原理、精确度、例子来分析
一、原理
通俗解释:计时好比数数, 在iOS中, 数数的人是系统内核,内核会根据一些设定好的条件 (比如按时) 产生相应事件, 然后通过回调函数向外抛出 (可理解为"报时"). 我们通过注册观察者来监听取得这些回调, 从而达到计时的目的。
注意重点1:这些与时间相关的事件的载体叫做事件源 (Source), iOS中有两种 Source: Run Loop Source
和Dispatch Source
.
1、
Run Loop Source
会唤醒当前Run Loop, 然后执行回调函数.
关于Run Loop Source, 可参考之前的博文iOS开发之进阶篇(8)—— Run Loops.
2、Dispatch Source
也会产生一些特定的事件, 事件通过 block 自动加入到对应的 dispatch queue 中.
关于Dispatch Source, 可参考苹果文档.
注意重点2:也就是说, iOS中有两种计时方式, 分别对应Run Loop
和Dispatch (GCD)
中的两种计时器源.
1、 NSTimer (Timer) 和 CADisplayLink 都属于
Run Loop Source
.
2、 GCD 属于Dispatch Source
.
二、精确度
1. NSTimer (Timer)
Run Loop
中的计时器源会受到模式的影响, 如果模式不对则不会触发. 比如说添加到主线程中的NSTimer, 当滚动 Scroll View 的时候, 模式发生改变, NSTimer暂时失效.
而重复计时器会受到当前事务的影响, 可能在一定范围内产生偏差, 偏差大小则和计时器的tolerance属性有关.
如果触发时间延迟得太久, 以致错过了一个或多个计划的触发时间, 则 timer 将在错过的时间段内仅触发一次.
2. CADisplayLink
CADisplaylink 也是由Run Loop
中的计时器源触发, 它与NSTimer相似, 都可以以一定的时间间隔触发回调 selector. 不同的是, CADisplaylink 的时间间隔是与屏幕的刷新频率相关联的.
CADisplayLink 的时间间隔由preferredFramesPerSecond属性来决定的, 意为每秒刷新的帧数. 如果设备的最大刷新率是每秒60帧, 则实际帧速率包括每秒15、20、30和60帧.
如果你设置为一个特定的值, 他么这个值最终也会和15、20、30以及60之间做最合适匹配. 比如设置为26或35, 则实际帧速率为30.
iOS设备的屏幕刷新频率是固定的, 因此CADisplayLink在正常情况下会在每次刷新结束都被调用, 精确度相当高. 但不要在CADisplayLink计时器中做耗时操作, 因为可能会被系统忽略掉.
3. 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)
#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
2、CADisplayLink
#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");
}
3、GCD
@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;
}
}
好文章值得分享:
1、iOS开发之进阶篇(10)—— Timer