定時器用于延遲一段時間或在指定時間點執(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];
}
- (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() 用于獲取機器時間(單位是納秒)丰滑,測試代碼來源于網絡顾犹,其功能展示了高精度定時器與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中任何定時器的精度竖般,都只是個參考值。
推薦文章:
Sign In With Apple(一)
算法小專欄:動態(tài)規(guī)劃(一)
Dart基礎(一)
Dart基礎(二)
Dart基礎(三)
Dart基礎(四)
iOS 短信驗證碼倒計時按鈕
iOS 環(huán)境變量配置
iOS 中處理定時任務的常用方法