

iOS 中精确定时的常用方法
source link: https://www.tuicool.com/articles/y6zY73F
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

级别: ★☆☆☆☆
标签:「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 <QuartzCore/QuartzCore.h> @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 <mach/mach.h> #include <mach/mach_time.h> #include <pthread.h> 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 <mach/mach.h> #include <mach/mach_time.h> 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(微信公众号)
Recommend
-
58
牛仔裤可以说是最「犯贱」的衣服种类了,明明就是新买的,还非偏要弄成穿了好几年的样子,才显得好像有在「活动」。但要做出这些水洗、石洗、刷白、破洞、补丁等各式各样的外貌对 Levi's 来说也是件颇头疼的事,一个小时最多只能处理两三条裤子不说,还需要上千种...
-
57
-
45
狄拉克方程是一个场方程,狄拉克波函数其实不是量子力学意义上的波函数,但是,可以视作是量子力学和量子场论之间的一种过渡.检验一个量子力学波动方程的一个重要的标志就是在氢原子的精确能级问题上的有效性,这一点…
-
94
1.背景 页面停留时间(Time on Page)简称 Tp,是网站分析中很常见的一个指标,用于反映用户在某些页面上停留时间的长短,传统的Tp统计方法会存在一定的统计盲区,比如无法监控单页应用,没有考虑用户切换Tab、最小化窗口等...
-
45
在 WWDC 2016 和 2017 都有提到启动这块的原理和性能优化思路,可见启动时间,对于开发者和用户们来说是多么的重要,本文就谈谈如何精确的度量 App 的启动时间,启动时间由 main 之前的启动时间和 main 之后的启动时间两部分组成。
-
36
如果要统计一篇文章的阅读量,可以直接使用 Redis 的 incr 指令来完成。如果要求阅读量必须按用户去重,那就可以使用 set 来记录阅读了这篇文章的所有用户 id,获取 set 集合的长度就是去重阅读量。但是如果爆款文章阅读量太大,set 会浪费太多存储
-
11
2020-07-08 • 于 代码库 阅读 20 iOS 正则匹配常用方法 验证手机号 123
-
17
Overview前段时间整理了
-
7
因为计算机数字是浮点型,所以在计算过程中通常得到的并不是一个准确的数据,所以在做一些数组运算的时候比较头疼,我们这里就来写一下精确运算的方法 首先是加法 (这里以两个数据相加为例) function add(arg1, arg2) { arg1...
-
5
用于精确导航和场景重建的 3D 配准方法(ICRA 2021)作者:chaochaoSEU|来源:微信公众号:3D视觉工坊注1:文末附有【视觉SLAM...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK