iOS 中精確定時(shí)的常用方法

級別: ★☆☆☆☆
標(biāo)簽:「iOS」「定時(shí) 」
作者: dac_1033
審校: QiShare團(tuán)隊(duì)


定時(shí)器用于延遲一段時(shí)間或在指定時(shí)間點(diǎn)執(zhí)行特定的代碼,之前我們介紹過iOS中處理定時(shí)任務(wù)常用方法,通過不同方法創(chuàng)建的定時(shí)器,其可靠性與精度都有不同。

  1. 定時(shí)器與runLoop:定時(shí)器NSTimer、CADisplayLink,底層基本都是由 runLoop 支持的。iOS中每個(gè)線程內(nèi)部都會(huì)有一個(gè)NSRunLoop ,可以通過[NSRunLoop currentRunLoop]獲取當(dāng)前線程中的runLoop ,二者是一一對應(yīng)關(guān)系。runLoop 啟動(dòng)之后,就能夠讓線程在沒有消息時(shí)休眠,在有消息時(shí)被喚醒并處理消息,避免資源長期被占用。定時(shí)器可以作為資源被 add 到 runLoop 中,受runLoop循環(huán)的控制及影響。
  2. 可靠性指是否嚴(yán)格按照設(shè)定的時(shí)間間隔按時(shí)執(zhí)行selector;精度指支持的最小時(shí)間間隔是多少,對程序中的定時(shí)器而言,由于線程的切換,處理任務(wù)的耗時(shí)程度不同,可靠性和精度只是參考值。

1. NSTimer的精度

影響NSTimer的執(zhí)行selector的因素:NSTimer被添加到特定mode的runLoop中;該mode型的runloop正在運(yùn)行;到達(dá)激發(fā)時(shí)間。 runLoop 切換模式時(shí),NSTimer 如果處于default模式下可能不會(huì)被觸發(fā)。每個(gè) runLoop 的循環(huán)間隔也無法保證,一般時(shí)間間隔限制為50-100毫秒比較合理,如果某個(gè)任務(wù)比較耗時(shí),runLoop 的處理下一個(gè)就會(huì)被順延,也就是說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

實(shí)驗(yàn)設(shè)置:在代碼中我們只通過NSLog打印了兩次執(zhí)行onTimeout的時(shí)間差,我們通過對比ts - lastTS與QiNSTimerInterval的值、1s內(nèi)執(zhí)行次數(shù),來確定NSTimer可否滿足QiNSTimerInterval這個(gè)精度。
注意:我們避免了onTimeout任何耗時(shí)操作,從而盡量保證NSLog打印出的定時(shí)的精確性。

//// 實(shí)驗(yàn)結(jié)果:

// QiNSTimerInterval為0.01時(shí)
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時(shí)
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時(shí)
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

說明:
在設(shè)置不同timeInterval值實(shí)驗(yàn)時(shí),對比log左側(cè)時(shí)間戳及l(fā)og數(shù)量。當(dāng)QiNSTimerInterval為0.001時(shí),1秒鐘內(nèi)打印了1000條log,兩條log的時(shí)間間隔可控,也即NSTimer允許1ms的時(shí)間精度。當(dāng)QiNSTimerInterval為0.0001時(shí),進(jìn)行以上對比,數(shù)據(jù)出現(xiàn)偏差。因此,我們得出,理想狀態(tài)下NSTimer的精度為1ms。

注意:

  1. NSTimer的時(shí)間精度雖然為1ms,但是只是理想狀態(tài)下,任何操作都可能會(huì)使onTimeout延時(shí)執(zhí)行。例如,現(xiàn)實(shí)中,我們在界面輸出一個(gè)倒計(jì)時(shí),如果設(shè)置QiNSTimerInterval為0.001,界面中秒位的變化明顯變慢,正常使用NSTimer進(jìn)行毫秒刷新時(shí),一般只精確到100ms才不會(huì)感到異常。
  2. 在一定程度上保證timer“準(zhǔn)時(shí)”的方法:在子線程中創(chuàng)建timer,在子線程中進(jìn)行定時(shí)任務(wù)的操作,需要UI操作時(shí)切換回主線程進(jìn)行操作;或者在子線程中創(chuàng)建timer,在主線程進(jìn)行定時(shí)任務(wù)的操作。

2. GCDTimer 的精度

回顧一下 GCDTimer 的基本實(shí)現(xiàn)過程:

// 1. 創(chuàng)建 dispatch source,指定檢測事件為定時(shí)
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue("Timer_Queue", 0));
// 2. 設(shè)置定時(shí)器啟動(dòng)時(shí)間、間隔
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 0.5 * NSEC_PER_SEC,  0 * NSEC_PER_SEC); 
// 3. 設(shè)置callback
dispatch_source_set_event_handler(timer, ^{
        NSLog(@"timer fired");
    });
dispatch_source_set_event_handler(timer, ^{
       //取消定時(shí)器時(shí)一些操作
    });
// 4. 啟動(dòng)定時(shí)器(剛創(chuàng)建的source處于被掛起狀態(tài))
dispatch_resume(timer);
// 5. 暫停定時(shí)器
dispatch_suspend(timer);
// 6. 取消定時(shí)器
dispatch_source_cancel(timer);
timer = nil;

GCDTimer相較于NSTimer的代碼處理過程優(yōu)點(diǎn)很明顯,NSTimer必須保證有一個(gè)活躍的runloop、創(chuàng)建與撤銷必須在同一個(gè)線程操作、內(nèi)存管理有潛在泄露的風(fēng)險(xiǎn)等,從上面的實(shí)現(xiàn)過程就可以看出使用GCDTimer基本沒有這些顧慮。按照NSTimer的測試邏輯對GCDTimer也進(jìn)行相應(yīng)測試,代碼如下:

#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

測試結(jié)果及應(yīng)說明的事項(xiàng)基本與NSTimer一致。

3. CADisplayLink

CADisplayLink 屬于 QuartzCore框架,它調(diào)用間隔與屏幕刷新頻率一致,每秒 60 幀,間隔 16.67ms。 當(dāng)需與顯示更新同步的定時(shí)時(shí)(如刷新界面動(dòng)畫等),建議CADisplayLink,可以省去一些多余的計(jì)算。我們之前沒有介紹過CADisplayLink,下面我們看一下CADisplayLink的用法和精度:

3.1 調(diào)用形式
- (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 幾個(gè)屬性
  • frameInterval
    表示間隔多少幀調(diào)用一次selector,默認(rèn)為1,即每幀都調(diào)用一次。官方文檔中強(qiáng)調(diào),當(dāng)該值被設(shè)定小于1時(shí),結(jié)果是不可預(yù)知的。
  • duration
    表示兩次屏幕刷新之間的時(shí)間間隔,只讀屬性,該屬性在target的selector被首次調(diào)用以后才會(huì)被賦值,我們可以計(jì)算出selector的調(diào)用間隔時(shí)間為duration * frameInterval。
    現(xiàn)存的iOS設(shè)備屏幕的刷新頻率為60Hz,這一點(diǎn)可以從CADisplayLink的duration屬性看出來。duration的值為1/60,即0.166666...
  • timestamp
    表示屏幕顯示的上一幀的時(shí)間戳,只讀屬性,CFTimeInterval類型,該屬性通常被target用來計(jì)算下一幀中應(yīng)該顯示的內(nèi)容。
  • preferredFramesPerSecond
    可以通過該屬性來設(shè)置CADisplayLink每秒刷新次數(shù),默認(rèn)值為屏幕最大幀率60Hz,如果在特定幀率內(nèi)無法提供對象的操作,可以通過降低幀率解決,實(shí)際的屏幕幀率會(huì)和手動(dòng)設(shè)置的preferredFramesPerSecond值有一定的出入。
3.3 CADisplayLink的精度

iOS設(shè)備的屏幕刷新頻率(FPS)是60Hz,CADisplayLink調(diào)用間隔與屏幕刷新頻率一致,即最小精度為 16.67 ms。

同樣按照NSTimer的測試邏輯對CADisplayLink也進(jìn)行相應(yīng)測試,代碼如下:

#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
//// 測試結(jié)果
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

注意:

  1. 理想狀態(tài)下,1s內(nèi)執(zhí)行60次,最小精度為16.7ms左右,精度誤差一般在 0.1 ~ 0.5 毫秒之間,精度比 NSTimer 要高。CADisplayLink運(yùn)行在主線程中在耗時(shí)任務(wù)之后,精度也不可控,需要借助多線程處理。
  2. 如果想保證精度,需要先確保任務(wù)能夠在最小時(shí)間間隔內(nèi)執(zhí)行完成,CADisplayLink 就比較可靠(例如毫秒級倒計(jì)時(shí),這種比較簡單非耗時(shí)任務(wù)可以保證質(zhì)量,但是每次倒計(jì)時(shí)應(yīng)以16.7ms為單位累加)。

4. iOS/OS X 中的高精度定時(shí)器

上述的幾種定時(shí)器雖然形式與用法不一,但核心邏輯實(shí)際是一樣的,都受限于蘋果為提高性能采用的各種策略,可能導(dǎo)致下一次無法實(shí)時(shí)地執(zhí)行selector。如果你確有需求要使用更高精度的定時(shí)器(一般視頻/音頻、精確幀速率的游戲等相關(guān)數(shù)據(jù)流操作中會(huì)需要),蘋果也提供了相應(yīng)方法 iOS/OS X 中的高精度定時(shí)器。這里說的高精度定時(shí)器與之前介紹的幾個(gè)定時(shí)器處理邏輯不一樣,它是基于高優(yōu)先級的線程調(diào)度類創(chuàng)建的定時(shí)器,在沒有多線程沖突的情況下,這類定時(shí)器的請求會(huì)被優(yōu)先處理。

iOS/OS X 中的高精度定時(shí)器邏輯:把定時(shí)器所在的線程,移到高優(yōu)先級的線程調(diào)度類;使用底層更精確的計(jì)時(shí)器API(以CPU時(shí)鐘為參照的計(jì)時(shí)API)。

4.1 使用過程
  • 將計(jì)時(shí)線程,調(diào)度為實(shí)時(shí)線程
    把定時(shí)器所在的線程,移到高優(yōu)先級的線程調(diào)度類,即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);
    }
}
  • 會(huì)用到的計(jì)時(shí)API
    使用更精確的計(jì)時(shí)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 該定時(shí)器的精度

mach_absolute_time() 用于獲取機(jī)器時(shí)間(單位是納秒),測試代碼來源于網(wǎng)絡(luò),其功能展示了高精度定時(shí)器與NSTimer的對比。

5. 總結(jié)

  1. NSTimer 最常用,需要注意的就是加入的 runLoop 的 Mode ,若是子線程,需要手動(dòng) run 這個(gè) RunLoop ;同時(shí)注意使用 invalidate 手動(dòng)停止定時(shí),否則引起內(nèi)存泄漏;NSTimer的創(chuàng)建與撤銷必須在同一個(gè)線程操作,不能跨越線程操作;
  2. GCD Timer 較 NSTimer 精度高,一般用于對文件資源等定期讀寫操作很方便,使用時(shí)需要注意 dispatch_resume 與 dispatch_suspend 配套,并且要給 dispatch source 設(shè)置新值或者置nil,需先 dispatch_source_cancel(timer) ,否則會(huì)導(dǎo)致崩潰;
  3. 需與顯示更新同步的定時(shí),建議 CADisplayLink ,可以省去多余計(jì)算;
  4. 高精度定時(shí),一般視頻/音頻、精確幀速率的游戲等相關(guān)數(shù)據(jù)流操作中會(huì)需要;
  5. iOS中任何定時(shí)器的精度,都只是個(gè)參考值。

工程源碼GitHub地址


推薦文章:
Sign In With Apple(一)
算法小專欄:動(dòng)態(tài)規(guī)劃(一)
Dart基礎(chǔ)(一)
Dart基礎(chǔ)(二)
Dart基礎(chǔ)(三)
Dart基礎(chǔ)(四)
iOS 短信驗(yàn)證碼倒計(jì)時(shí)按鈕
iOS 環(huán)境變量配置
iOS 中處理定時(shí)任務(wù)的常用方法

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容