定时器用于延迟一段时间或在指定时间点执行特定的代码,之前我们介绍过iOS中处理定时任务常用方法,通过不同方法创建的定时器,其可靠性与精度都有不同。

 

 

  1. 定时器与runLoop:定时器NSTimer、CADisplayLink,底层基本都是由 runLoop 支持的。iOS中每个线程内部都会有一个NSRunLoop ,可以通过[NSRunLoop currentRunLoop]获取当前线程中的runLoop ,二者是一一对应关系。runLoop 启动之后,就能够让线程在没有消息时休眠,在有消息时被唤醒并处理消息,避免资源长期被占用。定时器可以作为资源被 add 到 runLoop 中,受runLoop循环的控制及影响。

  2. 可靠性指是否严格按照设定的时间间隔按时执行selector;精度指支持的最小时间间隔是多少,对程序中的定时器而言,由于线程的切换,处理任务的耗时程度不同,可靠性和精度只是参考值。

1

NSTimer的精度

影响NSTimer的执行selector的因素:NSTimer被添加到特定mode的runLoop中;该mode型的runloop正在运行;到达激发时间。runLoop 切换模式时,NSTimer 如果处于default模式下可能不会被触发。每个 runLoop 的循环间隔也无法保证,一般时间间隔限制为50-100毫秒比较合理,如果某个任务比较耗时,runLoop 的处理下一个就会被顺延,也就是说NSTimer但并不可靠。

测试代码:

  1. #import "QiNSTimer.h"

  2.  

  3. #define QiNSTimerInterval 0.0001

  4.  

  5. @interface QiNSTimer ()

  6.  

  7. @property (nonatomic, strong) NSTimer *timer;

  8. @property (nonatomic, strong) NSLock *lock;

  9.  

  10. @property (nonatomic, assign) NSInteger count;

  11. @property (nonatomic, assign) NSTimeInterval lastTS;

  12.  

  13. @end

  14.  

  15. @implementation QiNSTimer

  16.  

  17.  

  18. #pragma mark - NSTimer Methods

  19.  

  20. - (void)resumeTimer {

  21.  

  22. if (_timer) {

  23. [self pauseTimer];

  24. }

  25. _timer = [NSTimer scheduledTimerWithTimeInterval:QiNSTimerInterval target:self selector:@selector(onTimeout:) userInfo:nil repeats:YES];

  26. [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];

  27. [[NSRunLoop currentRunLoop] run];

  28. [_timer fire];

  29. }

  30.  

  31. - (void)pauseTimer {

  32.  

  33. [_timer invalidate];

  34. _timer = nil;

  35. }

  36.  

  37. - (void)onTimeout:(NSTimer *)sender {

  38.  

  39. NSTimeInterval ts = [[NSDate date] timeIntervalSince1970];

  40. NSLog(@"---QiNSTimer--->>%ld %.5f", (long)_count++, ts - _lastTS);

  41. _lastTS = ts;

  42. }

  43.  

  44. @end

实验设置:在代码中我们只通过NSLog打印了两次执行onTimeout的时间差,我们通过对比ts - lastTS与QiNSTimerInterval的值、1s内执行次数,来确定NSTimer可否满足QiNSTimerInterval这个精度。注意:我们避免了onTimeout任何耗时操作,从而尽量保证NSLog打印出的定时的精确性。

  1. //// 实验结果:

  2.  

  3. // QiNSTimerInterval为0.01时

  4. 2019-07-22 18:42:50.516502+0800 QiTimer[1063:226400] ---QiNSTimer--->>1 0.01002

  5. 2019-07-22 18:42:50.526461+0800 QiTimer[1063:226400] ---QiNSTimer--->>2 0.00996

  6. 2019-07-22 18:42:50.536480+0800 QiTimer[1063:226400] ---QiNSTimer--->>3 0.01002

  7. .

  8. .

  9. .

  10. 2019-07-22 18:42:51.506502+0800 QiTimer[1063:226400] ---QiNSTimer--->>100 0.01055

  11. 2019-07-22 18:42:51.516437+0800 QiTimer[1063:226400] ---QiNSTimer--->>101 0.00998

  12. 2019-07-22 18:42:51.526183+0800 QiTimer[1063:226400] ---QiNSTimer--->>102 0.00974

  13.  

  14. // QiNSTimerInterval为0.001时

  15. 2019-07-22 18:45:59.655696+0800 QiTimer[1075:227871] ---QiNSTimer--->>1 0.00095

  16. 2019-07-22 18:45:59.656705+0800 QiTimer[1075:227871] ---QiNSTimer--->>2 0.00101

  17. 2019-07-22 18:45:59.657709+0800 QiTimer[1075:227871] ---QiNSTimer--->>3 0.00100

  18. .

  19. .

  20. .

  21. 2019-07-22 18:46:00.654778+0800 QiTimer[1075:227871] ---QiNSTimer--->>1000 0.00104

  22. 2019-07-22 18:46:00.655737+0800 QiTimer[1075:227871] ---QiNSTimer--->>1001 0.00096

  23. 2019-07-22 18:46:00.656741+0800 QiTimer[1075:227871] ---QiNSTimer--->>1002 0.00100

  24.  

  25. // QiNSTimerInterval为0.0001时

  26. 2019-07-22 18:48:07.960160+0800 QiTimer[1085:228783] ---QiNSTimer--->>1 0.00040

  27. 2019-07-22 18:48:07.960422+0800 QiTimer[1085:228783] ---QiNSTimer--->>2 0.00027

  28. 2019-07-22 18:48:07.960646+0800 QiTimer[1085:228783] ---QiNSTimer--->>3 0.00022

  29. .

  30. .

  31. .

  32. 2019-07-22 18:48:09.316050+0800 QiTimer[1085:228783] ---QiNSTimer--->>10001 0.00012

  33. 2019-07-22 18:48:09.316157+0800 QiTimer[1085:228783] ---QiNSTimer--->>10002 0.00011

  34. 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。

注意:

  1. NSTimer的时间精度虽然为1ms,但是只是理想状态下,任何操作都可能会使onTimeout延时执行。例如,现实中,我们在界面输出一个倒计时,如果设置QiNSTimerInterval为0.001,界面中秒位的变化明显变慢,正常使用NSTimer进行毫秒刷新时,一般只精确到100ms才不会感到异常。

  2. 在一定程度上保证timer“准时”的方法:在子线程中创建timer,在子线程中进行定时任务的操作,需要UI操作时切换回主线程进行操作;或者在子线程中创建timer,在主线程进行定时任务的操作。

2

GCDTimer 的精度

回顾一下 GCDTimer 的基本实现过程:

  1. // 1. 创建 dispatch source,指定检测事件为定时

  2. dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue("Timer_Queue", 0));

  3. // 2. 设置定时器启动时间、间隔

  4. dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 0.5 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);

  5. // 3. 设置callback

  6. dispatch_source_set_event_handler(timer, ^{

  7. NSLog(@"timer fired");

  8. });

  9. dispatch_source_set_event_handler(timer, ^{

  10. //取消定时器时一些操作

  11. });

  12. // 4. 启动定时器(刚创建的source处于被挂起状态)

  13. dispatch_resume(timer);

  14. // 5. 暂停定时器

  15. dispatch_suspend(timer);

  16. // 6. 取消定时器

  17. dispatch_source_cancel(timer);

  18. timer = nil;

GCDTimer相较于NSTimer的代码处理过程优点很明显,NSTimer必须保证有一个活跃的runloop、创建与撤销必须在同一个线程操作、内存管理有潜在泄露的风险等,从上面的实现过程就可以看出使用GCDTimer基本没有这些顾虑。按照NSTimer的测试逻辑对GCDTimer也进行相应测试,代码如下:

  1. #import "QiGCDTimer.h"

  2.  

  3. @interface QiGCDTimer ()

  4.  

  5. @property (strong, nonatomic) dispatch_source_t timer;

  6.  

  7. @property (nonatomic, assign) NSInteger count;

  8. @property (nonatomic, assign) NSTimeInterval lastTS;

  9.  

  10. @end

  11.  

  12. @implementation QiGCDTimer

  13.  

  14. + (QiGCDTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats queue:(dispatch_queue_t)queue block:(void (^)(void))block {

  15.  

  16. QiGCDTimer *timer = [[QiGCDTimer alloc] initWithInterval:interval repeats:repeats queue:queue block:block];

  17. return timer;

  18. }

  19.  

  20. - (instancetype)initWithInterval:(NSTimeInterval)interval repeats:(BOOL)repeats queue:(dispatch_queue_t)queue block:(void (^)(void))block {

  21.  

  22. self = [super init];

  23. if (self) {

  24. _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);

  25. dispatch_source_set_timer(self.timer, dispatch_time(DISPATCH_TIME_NOW, interval * NSEC_PER_SEC), interval * NSEC_PER_SEC, 0);

  26. dispatch_source_set_event_handler(self.timer, ^{

  27. if (!repeats) {

  28. dispatch_source_cancel(self.timer);

  29. }

  30. block();

  31.  

  32.  

  33. //// 测试

  34. [self onTimeout];

  35. });

  36. dispatch_resume(self.timer);

  37. }

  38. return self;

  39. }

  40.  

  41. - (void)dealloc {

  42.  

  43. [self invalidate];

  44. }

  45.  

  46. - (void)invalidate {

  47.  

  48. if (self.timer) {

  49. dispatch_source_cancel(self.timer);

  50. }

  51. }

  52.  

  53. - (void)onTimeout {

  54.  

  55. NSTimeInterval ts = [[NSDate date] timeIntervalSince1970];

  56. NSLog(@"---QiGCDTimer--->>%ld %.5f", (long)_count++, ts - _lastTS);

  57. _lastTS = ts;

  58. }

  59.  

  60. @end

测试结果及应说明的事项基本与NSTimer一致。

3

CADisplayLink

CADisplayLink 属于 QuartzCore框架,它调用间隔与屏幕刷新频率一致,每秒 60 帧,间隔 16.67ms。当需与显示更新同步的定时时(如刷新界面动画等),建议CADisplayLink,可以省去一些多余的计算。我们之前没有介绍过CADisplayLink,下面我们看一下CADisplayLink的用法和精度:

3.1 调用形式

  1. - (void)resumeCADisplayLink {

  2.  

  3. _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(rotate)];

  4. _displayLink.frameInterval = 1;

  5. [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];

  6. }

  7.  

  8. - (void) pauseCADisplayLink {

  9.  

  10. [_displayLink invalidate];

  11. _displayLink = nil;

  12. }

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也进行相应测试,代码如下:

  1. #import "QiCADisplayLink.h"

  2. #import <QuartzCore/QuartzCore.h>

  3.  

  4. @interface QiCADisplayLink ()

  5.  

  6. @property (nonatomic, strong) CADisplayLink *displayLink;

  7.  

  8. @property (nonatomic, assign) NSInteger count;

  9. @property (nonatomic, assign) NSTimeInterval lastTS;

  10.  

  11. @end

  12.  

  13. @implementation QiCADisplayLink

  14.  

  15.  

  16. #pragma mark - NSTimer Methods

  17.  

  18. - (void)resumeDisplayLink {

  19.  

  20. _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(onTimeout)];

  21. [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];

  22. }

  23.  

  24. - (void)pauseDisplayLink {

  25.  

  26. [_displayLink invalidate];

  27. _displayLink = nil;

  28. }

  29.  

  30.  

  31. - (void)onTimeout {

  32.  

  33. NSTimeInterval ts = [[NSDate date] timeIntervalSince1970];

  34. NSLog(@"---QiCADisplayLink--->>%ld %.5f", (long)_count++, ts - _lastTS);

  35. _lastTS = ts;

  36. }

  37.  

  38. @end

  1. //// 测试结果

  2. 2019-07-23 10:10:49.027269+0800 QiTimer[659:82685] ---QiCADisplayLink--->>1 0.01681

  3. 2019-07-23 10:10:49.043827+0800 QiTimer[659:82685] ---QiCADisplayLink--->>2 0.01659

  4. 2019-07-23 10:10:49.060542+0800 QiTimer[659:82685] ---QiCADisplayLink--->>3 0.01671

  5. .

  6. .

  7. .

  8. 2019-07-23 10:10:50.010421+0800 QiTimer[659:82685] ---QiCADisplayLink--->>60 0.01664

  9. 2019-07-23 10:10:50.027155+0800 QiTimer[659:82685] ---QiCADisplayLink--->>61 0.01673

  10. 2019-07-23 10:10:50.043830+0800 QiTimer[659:82685] ---QiCADisplayLink--->>62 0.01669

注意:

  1. 理想状态下,1s内执行60次,最小精度为16.7ms左右,精度误差一般在 0.1 ~ 0.5 毫秒之间,精度比 NSTimer 要高。CADisplayLink运行在主线程中在耗时任务之后,精度也不可控,需要借助多线程处理。

  2. 如果想保证精度,需要先确保任务能够在最小时间间隔内执行完成,CADisplayLink 就比较可靠(例如毫秒级倒计时,这种比较简单非耗时任务可以保证质量,但是每次倒计时应以16.7ms为单位累加)。

4

iOS/OS X 中的高精度定时器

上述的几种定时器虽然形式与用法不一,但核心逻辑实际是一样的,都受限于苹果为提高性能采用的各种策略,可能导致下一次无法实时地执行selector。如果你确有需求要使用更高精度的定时器(一般视频/音频、精确帧速率的游戏等相关数据流操作中会需要),苹果也提供了相应方法 iOS/OS X 中的高精度定时器。这里说的高精度定时器与之前介绍的几个定时器处理逻辑不一样,它是基于高优先级的线程调度类创建的定时器,在没有多线程冲突的情况下,这类定时器的请求会被优先处理。

iOS/OS X 中的高精度定时器逻辑:把定时器所在的线程,移到高优先级的线程调度类;使用底层更精确的计时器API(以CPU时钟为参照的计时API)。

4.1 使用过程

  • 将计时线程,调度为实时线程 把定时器所在的线程,移到高优先级的线程调度类,即the real time scheduling class中:

  1. #include <mach/mach.h>

  2. #include <mach/mach_time.h>

  3. #include <pthread.h>

  4.  

  5. void move_pthread_to_realtime_scheduling_class(pthread_t pthread)

  6. {

  7. mach_timebase_info_data_t timebase_info;

  8. mach_timebase_info(&timebase_info);

  9.  

  10. const uint64_t NANOS_PER_MSEC = 1000000ULL;

  11. double clock2abs = ((double)timebase_info.denom / (double)timebase_info.numer) * NANOS_PER_MSEC;

  12.  

  13. thread_time_constraint_policy_data_t policy;

  14. policy.period = 0;

  15. policy.computation = (uint32_t)(5 * clock2abs); // 5 ms of work

  16. policy.constraint = (uint32_t)(10 * clock2abs);

  17. policy.preemptible = FALSE;

  18.  

  19. int kr = thread_policy_set(pthread_mach_thread_np(pthread_self()),

  20. THREAD_TIME_CONSTRAINT_POLICY,

  21. (thread_policy_t)&policy,

  22. THREAD_TIME_CONSTRAINT_POLICY_COUNT);

  23. if (kr != KERN_SUCCESS) {

  24. mach_error("thread_policy_set:", kr);

  25. exit(1);

  26. }

  27. }

  • 会用到的计时API 使用更精确的计时API machwaituntil(),如下代码使用machwaituntil()等待10秒:

  1. #include <mach/mach.h>

  2. #include <mach/mach_time.h>

  3.  

  4. static const uint64_t NANOS_PER_USEC = 1000ULL;

  5. static const uint64_t NANOS_PER_MILLISEC = 1000ULL * NANOS_PER_USEC;

  6. static const uint64_t NANOS_PER_SEC = 1000ULL * NANOS_PER_MILLISEC;

  7.  

  8. static mach_timebase_info_data_t timebase_info;

  9.  

  10. static uint64_t abs_to_nanos(uint64_t abs) {

  11. return abs * timebase_info.numer / timebase_info.denom;

  12. }

  13.  

  14. static uint64_t nanos_to_abs(uint64_t nanos) {

  15. return nanos * timebase_info.denom / timebase_info.numer;

  16. }

  17.  

  18. void example_mach_wait_until(int argc, const char * argv[])

  19. {

  20. mach_timebase_info(&timebase_info);

  21. uint64_t time_to_wait = nanos_to_abs(10ULL * NANOS_PER_SEC);

  22. uint64_t now = mach_absolute_time();

  23. mach_wait_until(now + time_to_wait);

  24. }

4.2该定时器的精度

machabsolutetime() 用于获取机器时间(单位是纳秒),测试代码来源于网络,其功能展示了高精度定时器与NSTimer的对比。

5

总结

  1. NSTimer 最常用,需要注意的就是加入的 runLoop 的 Mode ,若是子线程,需要手动 run 这个 RunLoop ;同时注意使用 invalidate 手动停止定时,否则引起内存泄漏;NSTimer的创建与撤销必须在同一个线程操作,不能跨越线程操作;

  2. GCD Timer 较 NSTimer 精度高,一般用于对文件资源等定期读写操作很方便,使用时需要注意 dispatchresume 与 dispatchsuspend 配套,并且要给 dispatch source 设置新值或者置nil,需先 dispatchsourcecancel(timer) ,否则会导致崩溃;

  3. 需与显示更新同步的定时,建议 CADisplayLink ,可以省去多余计算;

  4. 高精度定时,一般视频/音频、精确帧速率的游戏等相关数据流操作中会需要;

  5. iOS中任何定时器的精度,都只是个参考值。

     

 

iOS 中精确定时的常用方法_iOS