定時器用于延遲一段時間或在指定時間點執(zhí)行特定的代碼,之前我們介紹過iOS中處理定時任務常用方法,通過不同方法創(chuàng)建的定時器,其可靠性與精度都有不同。
- 定時器與runLoop:定時器NSTimer、CADisplayLink,底層基本都是由 runLoop 支持的。iOS中每個線程內部都會有一個NSRunLoop ,可以通過[NSRunLoop currentRunLoop]獲取當前線程中的runLoop ,二者是一一對應關系。runLoop 啟動之后,就能夠讓線程在沒有消息時休眠,在有消息時被喚醒并處理消息,避免資源長期被占用。定時器可以作為資源被 add 到 runLoop 中,受runLoop循環(huán)的控制及影響。
- 可靠性指是否嚴格按照設定的時間間隔按時執(zhí)行selector;精度指支持的最小時間間隔是多少,對程序中的定時器而言,由于線程的切換,處理任務的耗時程度不同,可靠性和精度只是參考值。
1. NSTimer的精度
影響NSTimer的執(zhí)行selector的因素:NSTimer被添加到特定mode的runLoop中;該mode型的runloop正在運行;到達激發(fā)時間。 runLoop 切換模式時,NSTimer 如果處于default模式下可能不會被觸發(fā)。每個 runLoop 的循環(huán)間隔也無法保證,一般時間間隔限制為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打印了兩次執(zhí)行onTimeout的時間差,我們通過對比ts - _lastTS與QiNSTimerInterval的值、1s內執(zhí)行次數(shù),來確定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左側時間戳及l(fā)og數(shù)量。當QiNSTimerInterval為0.001時,1秒鐘內打印了1000條log,兩條log的時間間隔可控,也即NSTimer允許1ms的時間精度。當QiNSTimerInterval為0.0001時,進行以上對比,數(shù)據(jù)出現(xiàn)偏差。因此,我們得出,理想狀態(tài)下NSTimer的精度為1ms。
注意:
- NSTimer的時間精度雖然為1ms,但是只是理想狀態(tài)下,任何操作都可能會使onTimeout延時執(zhí)行。例如,現(xiàn)實中,我們在界面輸出一個倒計時,如果設置QiNSTimerInterval為0.001,界面中秒位的變化明顯變慢,正常使用NSTimer進行毫秒刷新時,一般只精確到100ms才不會感到異常。
- 在一定程度上保證timer“準時”的方法:在子線程中創(chuàng)建timer,在子線程中進行定時任務的操作,需要UI操作時切換回主線程進行操作;或者在子線程中創(chuàng)建timer,在主線程進行定時任務的操作。
2. GCDTimer 的精度
回顧一下 GCDTimer 的基本實現(xiàn)過程:
// 1. 創(chuàng)建 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. 啟動定時器(剛創(chuàng)建的source處于被掛起狀態(tài))
dispatch_resume(timer);
// 5. 暫停定時器
dispatch_suspend(timer);
// 6. 取消定時器
dispatch_source_cancel(timer);
timer = nil;
GCDTimer相較于NSTimer的代碼處理過程優(yōu)點很明顯,NSTimer必須保證有一個活躍的runloop、創(chuàng)建與撤銷必須在同一個線程操作、內存管理有潛在泄露的風險等,從上面的實現(xiàn)過程就可以看出使用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。
現(xiàn)存的iOS設備屏幕的刷新頻率為60Hz,這一點可以從CADisplayLink的duration屬性看出來。duration的值為1/60,即0.166666... - timestamp
表示屏幕顯示的上一幀的時間戳,只讀屬性,CFTimeInterval類型,該屬性通常被target用來計算下一幀中應該顯示的內容。 - preferredFramesPerSecond
可以通過該屬性來設置CADisplayLink每秒刷新次數(shù),默認值為屏幕最大幀率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
注意:
- 理想狀態(tài)下,1s內執(zhí)行60次,最小精度為16.7ms左右,精度誤差一般在 0.1 ~ 0.5 毫秒之間,精度比 NSTimer 要高。CADisplayLink運行在主線程中在耗時任務之后,精度也不可控,需要借助多線程處理。
- 如果想保證精度,需要先確保任務能夠在最小時間間隔內執(zhí)行完成,CADisplayLink 就比較可靠(例如毫秒級倒計時,這種比較簡單非耗時任務可以保證質量,但是每次倒計時應以16.7ms為單位累加)。
4. iOS/OS X 中的高精度定時器
上述的幾種定時器雖然形式與用法不一,但核心邏輯實際是一樣的,都受限于蘋果為提高性能采用的各種策略,可能導致下一次無法實時地執(zhí)行selector。如果你確有需求要使用更高精度的定時器(一般視頻/音頻、精確幀速率的游戲等相關數(shù)據(jù)流操作中會需要),蘋果也提供了相應方法 iOS/OS X 中的高精度定時器。這里說的高精度定時器與之前介紹的幾個定時器處理邏輯不一樣,它是基于高優(yōu)先級的線程調度類創(chuàng)建的定時器,在沒有多線程沖突的情況下,這類定時器的請求會被優(yōu)先處理。
iOS/OS X 中的高精度定時器邏輯:把定時器所在的線程,移到高優(yōu)先級的線程調度類;使用底層更精確的計時器API(以CPU時鐘為參照的計時API)。
4.1 使用過程
- 將計時線程,調度為實時線程
把定時器所在的線程,移到高優(yōu)先級的線程調度類,即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() 用于獲取機器時間(單位是納秒),測試代碼來源于網(wǎng)絡,其功能展示了高精度定時器與NSTimer的對比。
5. 總結
- NSTimer 最常用,需要注意的就是加入的 runLoop 的 Mode ,若是子線程,需要手動 run 這個 RunLoop ;同時注意使用 invalidate 手動停止定時,否則引起內存泄漏;NSTimer的創(chuàng)建與撤銷必須在同一個線程操作,不能跨越線程操作;
- GCD Timer 較 NSTimer 精度高,一般用于對文件資源等定期讀寫操作很方便,使用時需要注意 dispatch_resume 與 dispatch_suspend 配套,并且要給 dispatch source 設置新值或者置nil,需先 dispatch_source_cancel(timer) ,否則會導致崩潰;
- 需與顯示更新同步的定時,建議 CADisplayLink ,可以省去多余計算;
- 高精度定時,一般視頻/音頻、精確幀速率的游戲等相關數(shù)據(jù)流操作中會需要;
- iOS中任何定時器的精度,都只是個參考值。