iOS 中精确定时的常用方法
级别: ★☆☆☆☆
标签:「iOS」「定时 」
作者: dac_1033
审校: QiShare团队
定时器用于延迟一段时间或在指定时间点执行特定的代码,之前我们介绍过 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中任何定时器的精度,都只是个参考值。
小编微信:可加并拉入《QiShare技术交流群》。

关注我们的途径有:
QiShare(微信公众号)