定时器用于延迟一段时间或在指定时间点执行特定的代码,之前我们介绍过iOS中处理定时任务常用方法,通过不同方法创建的定时器,其可靠性与精度都有不同。
- 定时器与runLoop:定时器NSTimer、CADisplayLink,底层基本都是由 runLoop 支持的。iOS中每个线程内部都会有一个NSRunLoop ,可以通过[NSRunLoop currentRunLoop]获取当前线程中的runLoop ,二者是一一对应关系。runLoop 启动之后,就能够让线程在没有消息时休眠,在有消息时被唤醒并处理消息,避免资源长期被占用。定时器可以作为资源被 add 到 runLoop 中,受runLoop循环的控制及影响。
- 可靠性指是否严格按照设定的时间间隔按时执行selector;精度指支持的最小时间间隔是多少,对程序中的定时器而言,由于线程的切换,处理任务的耗时程度不同,可靠性和精度只是参考值。
1. NSTimer的精度
影响NSTimer的执行selector的因素:NSTimer被添加到特定mode的runLoop中;该mode型的runloop正在运行;到达激发时间。 runLoop 切换模式时,NSTimer 如果处于default模式下可能不会被触发。每个 runLoop 的循环间隔也无法保证,一般时间间隔限制为50-100毫秒比较合理,如果某个任务比较耗时,runLoop 的处理下一个就会被顺延,也就是说NSTimer但并不可靠。
测试代码:
#import "QiNSTimer.h"
#define QiNSTimerInterval 0.0001
@interface QiNSTimer ()
@property (nonatomic, strong) NSTimer *timer;
@property (nonatomic, strong) NSLock *lock;
@property (nonatomic, assign) NSInteger count;
@property (nonatomic, assign) NSTimeInterval lastTS;
@end
@implementation QiNSTimer
#pragma mark - NSTimer Methods
- (void)resumeTimer {
if (_timer) {
[self pauseTimer];
}
_timer = [NSTimer scheduledTimerWithTimeInterval:QiNSTimerInterval target:self selector:@selector(onTimeout:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
[[NSRunLoop currentRunLoop] run];
[_timer fire];
}
- (void)pauseTimer {
[_timer invalidate];
_timer = nil;
}
- (void)onTimeout:(NSTimer *)sender {
NSTimeInterval ts = [[NSDate date] timeIntervalSince1970];
NSLog(@"---QiNSTimer--->>%ld %.5f", (long)_count++, ts - _lastTS);
_lastTS = ts;
}
@end
实验设置:在代码中我们只通过NSLog打印了两次执行onTimeout的时间差,我们通过对比ts - _lastTS与QiNSTimerInterval的值、1s内执行次数,来确定NSTimer可否满足QiNSTimerInterval这个精度。
注意:我们避免了onTimeout任何耗时操作,从而尽量保证NSLog打印出的定时的精确性。
//// 实验结果:
// QiNSTimerInterval为0.01时
2019-07-22 18:42:50.516502+0800 QiTimer[1063:226400] ---QiNSTimer--->>1 0.01002
2019-07-22 18:42:50.526461+0800 QiTimer[1063:226400] ---QiNSTimer--->>2 0.00996
2019-07-22 18:42:50.536480+0800 QiTimer[1063:226400] ---QiNSTimer--->>3 0.01002
.
.
.
2019-07-22 18:42:51.506502+0800 QiTimer[1063:226400] ---QiNSTimer--->>100 0.01055
2019-07-22 18:42:51.516437+0800 QiTimer[1063:226400] ---QiNSTimer--->>101 0.00998
2019-07-22 18:42:51.526183+0800 QiTimer[1063:226400] ---QiNSTimer--->>102 0.00974
// QiNSTimerInterval为0.001时
2019-07-22 18:45:59.655696+0800 QiTimer[1075:227871] ---QiNSTimer--->>1 0.00095
2019-07-22 18:45:59.656705+0800 QiTimer[1075:227871] ---QiNSTimer--->>2 0.00101
2019-07-22 18:45:59.657709+0800 QiTimer[1075:227871] ---QiNSTimer--->>3 0.00100
.
.
.
2019-07-22 18:46:00.654778+0800 QiTimer[1075:227871] ---QiNSTimer--->>1000 0.00104
2019-07-22 18:46:00.655737+0800 QiTimer[1075:227871] ---QiNSTimer--->>1001 0.00096
2019-07-22 18:46:00.656741+0800 QiTimer[1075:227871] ---QiNSTimer--->>1002 0.00100
// QiNSTimerInterval为0.0001时
2019-07-22 18:48:07.960160+0800 QiTimer[1085:228783] ---QiNSTimer--->>1 0.00040
2019-07-22 18:48:07.960422+0800 QiTimer[1085:228783] ---QiNSTimer--->>2 0.00027
2019-07-22 18:48:07.960646+0800 QiTimer[1085:228783] ---QiNSTimer--->>3 0.00022
.
.
.
2019-07-22 18:48:09.316050+0800 QiTimer[1085:228783] ---QiNSTimer--->>10001 0.00012
2019-07-22 18:48:09.316157+0800 QiTimer[1085:228783] ---QiNSTimer--->>10002 0.00011
2019-07-22 18:48:09.316253+0800 QiTimer[1085:228783] ---QiNSTimer--->>10003 0.00009
说明:
在设置不同timeInterval值实验时,对比log左侧时间戳及log数量。当QiNSTimerInterval为0.001时,1秒钟内打印了1000条log,两条log的时间间隔可控,也即NSTimer允许1ms的时间精度。当QiNSTimerInterval为0.0001时,进行以上对比,数据出现偏差。因此,我们得出,理想状态下NSTimer的精度为1ms。
注意:
- NSTimer的时间精度虽然为1ms,但是只是理想状态下,任何操作都可能会使onTimeout延时执行。例如,现实中,我们在界面输出一个倒计时,如果设置QiNSTimerInterval为0.001,界面中秒位的变化明显变慢,正常使用NSTimer进行毫秒刷新时,一般只精确到100ms才不会感到异常。
- 在一定程度上保证timer“准时”的方法:在子线程中创建timer,在子线程中进行定时任务的操作,需要UI操作时切换回主线程进行操作;或者在子线程中创建timer,在主线程进行定时任务的操作。
2. GCDTimer 的精度
回顾一下 GCDTimer 的基本实现过程:
// 1. 创建 dispatch source,指定检测事件为定时
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue("Timer_Queue", 0));
// 2. 设置定时器启动时间、间隔
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 0.5 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
// 3. 设置callback
dispatch_source_set_event_handler(timer, ^{
NSLog(@"timer fired");
});
dispatch_source_set_event_handler(timer, ^{
//取消定时器时一些操作
});
// 4. 启动定时器(刚创建的source处于被挂起状态)
dispatch_resume(timer);
// 5. 暂停定时器
dispatch_suspend(timer);
// 6. 取消定时器
dispatch_source_cancel(timer);
timer = nil;
GCDTimer相较于NSTimer的代码处理过程优点很明显,NSTimer必须保证有一个活跃的runloop、创建与撤销必须在同一个线程操作、内存管理有潜在泄露的风险等,从上面的实现过程就可以看出使用GCDTimer基本没有这些顾虑。按照NSTimer的测试逻辑对GCDTimer也进行相应测试,代码如下:
#import "QiGCDTimer.h"
@interface QiGCDTimer ()
@property (strong, nonatomic) dispatch_source_t timer;
@property (nonatomic, assign) NSInteger count;
@property (nonatomic, assign) NSTimeInterval lastTS;
@end
@implementation QiGCDTimer
+ (QiGCDTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats queue:(dispatch_queue_t)queue block:(void (^)(void))block {
QiGCDTimer *timer = [[QiGCDTimer alloc] initWithInterval:interval repeats:repeats queue:queue block:block];
return timer;
}
- (instancetype)initWithInterval:(NSTimeInterval)interval repeats:(BOOL)repeats queue:(dispatch_queue_t)queue block:(void (^)(void))block {
self = [super init];
if (self) {
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_source_set_timer(self.timer, dispatch_time(DISPATCH_TIME_NOW, interval * NSEC_PER_SEC), interval * NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(self.timer, ^{
if (!repeats) {
dispatch_source_cancel(self.timer);
}
block();
//// 测试
[self onTimeout];
});
dispatch_resume(self.timer);
}
return self;
}
- (void)dealloc {
[self invalidate];
}
- (void)invalidate {
if (self.timer) {
dispatch_source_cancel(self.timer);
}
}
- (void)onTimeout {
NSTimeInterval ts = [[NSDate date] timeIntervalSince1970];
NSLog(@"---QiGCDTimer--->>%ld %.5f", (long)_count++, ts - _lastTS);
_lastTS = ts;
}
@end
测试结果及应说明的事项基本与NSTimer一致。
3. CADisplayLink
CADisplayLink 属于 QuartzCore框架,它调用间隔与屏幕刷新频率一致,每秒 60 帧,间隔 16.67ms。 当需与显示更新同步的定时时(如刷新界面动画等),建议CADisplayLink,可以省去一些多余的计算。我们之前没有介绍过CADisplayLink,下面我们看一下CADisplayLink的用法和精度:
3.1 调用形式
- (void)resumeCADisplayLink {
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(rotate)];
_displayLink.frameInterval = 1;
[_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}
- (void) pauseCADisplayLink {
[_displayLink invalidate];
_displayLink = nil;
}
3.2 几个属性
- frameInterval
表示间隔多少帧调用一次selector,默认为1,即每帧都调用一次。官方文档中强调,当该值被设定小于1时,结果是不可预知的。 - duration
表示两次屏幕刷新之间的时间间隔,只读属性,该属性在target的selector被首次调用以后才会被赋值,我们可以计算出selector的调用间隔时间为duration * frameInterval。
现存的iOS设备屏幕的刷新频率为60Hz,这一点可以从CADisplayLink的duration属性看出来。duration的值为1/60,即0.166666... - timestamp
表示屏幕显示的上一帧的时间戳,只读属性,CFTimeInterval类型,该属性通常被target用来计算下一帧中应该显示的内容。 - preferredFramesPerSecond
可以通过该属性来设置CADisplayLink每秒刷新次数,默认值为屏幕最大帧率60Hz,如果在特定帧率内无法提供对象的操作,可以通过降低帧率解决,实际的屏幕帧率会和手动设置的preferredFramesPerSecond值有一定的出入。
3.3 CADisplayLink的精度
iOS设备的屏幕刷新频率(FPS)是60Hz,CADisplayLink调用间隔与屏幕刷新频率一致,即最小精度为 16.67 ms。
同样按照NSTimer的测试逻辑对CADisplayLink也进行相应测试,代码如下:
#import "QiCADisplayLink.h"
#import
@interface QiCADisplayLink ()
@property (nonatomic, strong) CADisplayLink *displayLink;
@property (nonatomic, assign) NSInteger count;
@property (nonatomic, assign) NSTimeInterval lastTS;
@end
@implementation QiCADisplayLink
#pragma mark - NSTimer Methods
- (void)resumeDisplayLink {
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(onTimeout)];
[_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)pauseDisplayLink {
[_displayLink invalidate];
_displayLink = nil;
}
- (void)onTimeout {
NSTimeInterval ts = [[NSDate date] timeIntervalSince1970];
NSLog(@"---QiCADisplayLink--->>%ld %.5f", (long)_count++, ts - _lastTS);
_lastTS = ts;
}
@end
//// 测试结果
2019-07-23 10:10:49.027269+0800 QiTimer[659:82685] ---QiCADisplayLink--->>1 0.01681
2019-07-23 10:10:49.043827+0800 QiTimer[659:82685] ---QiCADisplayLink--->>2 0.01659
2019-07-23 10:10:49.060542+0800 QiTimer[659:82685] ---QiCADisplayLink--->>3 0.01671
.
.
.
2019-07-23 10:10:50.010421+0800 QiTimer[659:82685] ---QiCADisplayLink--->>60 0.01664
2019-07-23 10:10:50.027155+0800 QiTimer[659:82685] ---QiCADisplayLink--->>61 0.01673
2019-07-23 10:10:50.043830+0800 QiTimer[659:82685] ---QiCADisplayLink--->>62 0.01669
注意:
- 理想状态下,1s内执行60次,最小精度为16.7ms左右,精度误差一般在 0.1 ~ 0.5 毫秒之间,精度比 NSTimer 要高。CADisplayLink运行在主线程中在耗时任务之后,精度也不可控,需要借助多线程处理。
- 如果想保证精度,需要先确保任务能够在最小时间间隔内执行完成,CADisplayLink 就比较可靠(例如毫秒级倒计时,这种比较简单非耗时任务可以保证质量,但是每次倒计时应以16.7ms为单位累加)。
4. iOS/OS X 中的高精度定时器
上述的几种定时器虽然形式与用法不一,但核心逻辑实际是一样的,都受限于苹果为提高性能采用的各种策略,可能导致下一次无法实时地执行selector。如果你确有需求要使用更高精度的定时器(一般视频/音频、精确帧速率的游戏等相关数据流操作中会需要),苹果也提供了相应方法 iOS/OS X 中的高精度定时器。这里说的高精度定时器与之前介绍的几个定时器处理逻辑不一样,它是基于高优先级的线程调度类创建的定时器,在没有多线程冲突的情况下,这类定时器的请求会被优先处理。
iOS/OS X 中的高精度定时器逻辑:把定时器所在的线程,移到高优先级的线程调度类;使用底层更精确的计时器API(以CPU时钟为参照的计时API)。
4.1 使用过程
- 将计时线程,调度为实时线程
把定时器所在的线程,移到高优先级的线程调度类,即the real time scheduling class中:
#include
#include
#include
void move_pthread_to_realtime_scheduling_class(pthread_t pthread)
{
mach_timebase_info_data_t timebase_info;
mach_timebase_info(&timebase_info);
const uint64_t NANOS_PER_MSEC = 1000000ULL;
double clock2abs = ((double)timebase_info.denom / (double)timebase_info.numer) * NANOS_PER_MSEC;
thread_time_constraint_policy_data_t policy;
policy.period = 0;
policy.computation = (uint32_t)(5 * clock2abs); // 5 ms of work
policy.constraint = (uint32_t)(10 * clock2abs);
policy.preemptible = FALSE;
int kr = thread_policy_set(pthread_mach_thread_np(pthread_self()),
THREAD_TIME_CONSTRAINT_POLICY,
(thread_policy_t)&policy,
THREAD_TIME_CONSTRAINT_POLICY_COUNT);
if (kr != KERN_SUCCESS) {
mach_error("thread_policy_set:", kr);
exit(1);
}
}
- 会用到的计时API
使用更精确的计时API mach_wait_until(),如下代码使用mach_wait_until()等待10秒:
#include
#include
static const uint64_t NANOS_PER_USEC = 1000ULL;
static const uint64_t NANOS_PER_MILLISEC = 1000ULL * NANOS_PER_USEC;
static const uint64_t NANOS_PER_SEC = 1000ULL * NANOS_PER_MILLISEC;
static mach_timebase_info_data_t timebase_info;
static uint64_t abs_to_nanos(uint64_t abs) {
return abs * timebase_info.numer / timebase_info.denom;
}
static uint64_t nanos_to_abs(uint64_t nanos) {
return nanos * timebase_info.denom / timebase_info.numer;
}
void example_mach_wait_until(int argc, const char * argv[])
{
mach_timebase_info(&timebase_info);
uint64_t time_to_wait = nanos_to_abs(10ULL * NANOS_PER_SEC);
uint64_t now = mach_absolute_time();
mach_wait_until(now + time_to_wait);
}
4.2 该定时器的精度
mach_absolute_time() 用于获取机器时间(单位是纳秒),测试代码来源于网络,其功能展示了高精度定时器与NSTimer的对比。
5. 总结
- NSTimer 最常用,需要注意的就是加入的 runLoop 的 Mode ,若是子线程,需要手动 run 这个 RunLoop ;同时注意使用 invalidate 手动停止定时,否则引起内存泄漏;NSTimer的创建与撤销必须在同一个线程操作,不能跨越线程操作;
- GCD Timer 较 NSTimer 精度高,一般用于对文件资源等定期读写操作很方便,使用时需要注意 dispatch_resume 与 dispatch_suspend 配套,并且要给 dispatch source 设置新值或者置nil,需先 dispatch_source_cancel(timer) ,否则会导致崩溃;
- 需与显示更新同步的定时,建议 CADisplayLink ,可以省去多余计算;
- 高精度定时,一般视频/音频、精确帧速率的游戏等相关数据流操作中会需要;
- iOS中任何定时器的精度,都只是个参考值。
工程源码GitHub地址