[iOS]各種定時器–最全的定時器使用

流火緋瞳發表於2017-06-14

說到定時器, 我們使用最多的就是NSTimerGCD 了, 還有另外一個高階的定時器 CADisplayLink;

一. NSTimer

NSTimer的初始化方法有以下幾種:
會自動啟動, 並加入 MainRunloop NSDefaultRunLoopMode 中,

注意: 這裡的自動啟動, 並不是馬上就會啟動, 而是會延遲大概一個interval的時間:

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block

引數:

  • internal : 時間間隔, 多久呼叫一次

  • repeats: 是否重複呼叫

  • block: 需要重複做的事情

使用:

 [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        
        static NSInteger num = 0;
        
        NSLog(@"%ld", (long)num);
        num++;
        
        if (num > 4) {
            
            [timer invalidate];
            
            NSLog(@"end");
        }
    }];
    
    NSLog(@"start");

這時, 控制檯的輸出:

2016-12-29 16:29:53.901 定時器[11673:278678] start
2016-12-29 16:29:54.919 定時器[11673:278678] 0
2016-12-29 16:29:55.965 定時器[11673:278678] 1
2016-12-29 16:29:56.901 定時器[11673:278678] 2
2016-12-29 16:29:57.974 定時器[11673:278678] 3
2016-12-29 16:29:58.958 定時器[11673:278678] 4
2016-12-29 16:29:58.959 定時器[11673:278678] end

可以看出, 這裡的internal設定為1s, 大概延遲了1s才開始執行block裡的內容;

這裡的停止定時器, 我直接在block裡進行的, 如果使用一個全域性變數來再其他地方手動停止定時器,需要這樣進行:

[self.timer invalidate];
self.timer = nil;

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo

引數:

示例:

// NSInvocation形式
- (void)timer2 {
    
    NSMethodSignature *method = [ViewController instanceMethodSignatureForSelector:@selector(invocationTimeRun:)];
    
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:method];
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 invocation:invocation repeats:YES];
    
    // 設定方法呼叫者
    invocation.target = self;
    
    // 這裡的SEL需要和NSMethodSignature中的一致
    invocation.selector = @selector(invocationTimeRun:);
    
    // 設定引數
    // //這裡的Index要從2開始,以為0跟1已經被佔據了,分別是self(target),selector(_cmd)
    // 如果有多個引數, 可依次設定3 4 5 ...
    [invocation setArgument:&timer atIndex:2];
    
    [invocation invoke];
    
    NSLog(@"start");
}
- (void)invocationTimeRun:(NSTimer *)timer {
    
    static NSInteger num = 0;
    NSLog(@"%ld---%@", (long)num, timer);
    
    num++;
    
    if (num > 4) {
        [timer invalidate];
    }
}

輸出:

2016-12-29 16:52:54.029 定時器[12089:289673] 0---<__NSCFTimer: 0x60000017d940>
2016-12-29 16:52:54.029 定時器[12089:289673] start
2016-12-29 16:52:55.104 定時器[12089:289673] 1---<__NSCFTimer: 0x60000017d940>
2016-12-29 16:52:56.095 定時器[12089:289673] 2---<__NSCFTimer: 0x60000017d940>
2016-12-29 16:52:57.098 定時器[12089:289673] 3---<__NSCFTimer: 0x60000017d940>
2016-12-29 16:52:58.094 定時器[12089:289673] 4---<__NSCFTimer: 0x60000017d940>

可以看出, 這裡定時器是立馬就執行了, 沒有延遲;
此方法可以傳遞多個引數, 下面是傳遞兩個引數的示例:

// NSInvocation形式
- (void)timer2 {
    
    NSMethodSignature *method = [ViewController instanceMethodSignatureForSelector:@selector(invocationTimeRun:des:)];
    
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:method];
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 invocation:invocation repeats:YES];
    
    // 設定方法呼叫者
    invocation.target = self;
    
    // 這裡的SEL需要和NSMethodSignature中的一致
    invocation.selector = @selector(invocationTimeRun:des:);
    
    // 設定引數
    // //這裡的Index要從2開始,以為0跟1已經被佔據了,分別是self(target),selector(_cmd)
    // 如果有多個引數, 可依次設定3 4 5 ...
    [invocation setArgument:&timer atIndex:2];
    // 設定第二個引數
    NSString *dsc = @"第二個引數是字串";
    [invocation setArgument:&dsc atIndex:3];
    
    [invocation invoke];
    
    NSLog(@"start");
}
- (void)invocationTimeRun:(NSTimer *)timer des:(NSString *)dsc {
    
    static NSInteger num = 0;
    NSLog(@"%ld---%@--%@", (long)num, timer, dsc);
    
    num++;
    
    if (num > 4) {
        [timer invalidate];
    }
}

輸出:

2016-12-29 16:57:45.087 定時器[12183:292324] 0---<__NSCFTimer: 0x60000016dbc0>--第二個引數是字串
2016-12-29 16:57:45.088 定時器[12183:292324] start
2016-12-29 16:57:46.161 定時器[12183:292324] 1---<__NSCFTimer: 0x60000016dbc0>--第二個引數是字串
2016-12-29 16:57:47.161 定時器[12183:292324] 2---<__NSCFTimer: 0x60000016dbc0>--第二個引數是字串
2016-12-29 16:57:48.150 定時器[12183:292324] 3---<__NSCFTimer: 0x60000016dbc0>--第二個引數是字串
2016-12-29 16:57:49.159 定時器[12183:292324] 4---<__NSCFTimer: 0x60000016dbc0>--第二個引數是字串
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo

引數:

  • ti: 時間間隔

  • aTarget: 呼叫者

  • aSelector: 執行的方法

  • userInfo: 引數

  • yesOrNo: 是否重複執行

示例:

- (void)timer3 {
    
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(targetRun:) userInfo:@"這是攜帶的引數" repeats:YES];
    
    NSLog(@"start");
}
- (void)targetRun:(NSTimer *)timer {
    
    static NSInteger num = 0;
    
    NSLog(@"%ld---%@--%@", (long)num, timer, timer.userInfo);
    
    num++;
    
    if (num > 4) {
        [timer invalidate];
    }
}

輸出:

2016-12-29 17:05:11.590 定時器[12328:296879] start
2016-12-29 17:05:12.655 定時器[12328:296879] 0---<__NSCFTimer: 0x608000162700>--這是攜帶的引數
2016-12-29 17:05:13.661 定時器[12328:296879] 1---<__NSCFTimer: 0x608000162700>--這是攜帶的引數
2016-12-29 17:05:14.664 定時器[12328:296879] 2---<__NSCFTimer: 0x608000162700>--這是攜帶的引數
2016-12-29 17:05:15.651 定時器[12328:296879] 3---<__NSCFTimer: 0x608000162700>--這是攜帶的引數
2016-12-29 17:05:16.650 定時器[12328:296879] 4---<__NSCFTimer: 0x608000162700>--這是攜帶的引數

下面這三種方式建立定時器的用法, 和上面相應的方法類似, 需要注意的是, 這樣建立的定時器, 並不會執行, 需要我們手動來開啟定時器;

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo

開啟的方式是, 將當前定時器新增到RunLoop中:

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

下面給出一個示例:

- (void)timer4 {
    
    NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        
        static NSInteger num = 0;
        
        NSLog(@"%ld", (long)num);
        num++;
        
        if (num > 4) {
            
            [timer invalidate];
            timer = nil;
            
            NSLog(@"end");
        }
    }];
    
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    
    NSLog(@"start");
}

輸出:

2016-12-29 17:12:13.955 定時器[12498:301751] start
2016-12-29 17:12:15.013 定時器[12498:301751] 0
2016-12-29 17:12:16.018 定時器[12498:301751] 1
2016-12-29 17:12:17.011 定時器[12498:301751] 2
2016-12-29 17:12:18.024 定時器[12498:301751] 3
2016-12-29 17:12:19.023 定時器[12498:301751] 4
2016-12-29 17:12:19.023 定時器[12498:301751] end

定時器基本的建立方式就這些了, 還可以設定其他的屬性, 例如開啟時間, 這些直接參考其API 進行設定即可;

注意: 以上例項中, 我沒有使用全域性的NSTimer 物件, 如果設定全域性變數, 或者設定為屬性, 在停止定時器的時候要手動置為nil, 即:

[timer invalidate];
 timer = nil;

二. GCD

dispatch_after : 延遲執行一次
dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t block)

示例:

- (void)gcdTimer {
    
    // 延遲2s
    dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC);
    
    dispatch_after(delayTime, dispatch_get_main_queue(), ^(void){
        
        NSLog(@"延遲2s後執行");
    });
    
    NSLog(@"start");
}

重複執行的定時器

void
dispatch_source_set_timer(dispatch_source_t source,
    dispatch_time_t start,
    uint64_t interval,
    uint64_t leeway)

引數:

  • source: 定時器

  • start: 開始時間, 當我們使用 dispatch_time 或者 DISPATCH_TIME_NOW 時,系統會使用預設時鐘來進行計時。然而當系統休眠的時候,預設時鐘是不走的,也就會導致計時器停止。使用 dispatch_walltime 可以讓計時器按照真實時間間隔進行計時;

  • interval: 間隔(如果設定為 DISPATCH_TIME_FOREVER 則只執行一次)

  • leeway: 允許的誤差範圍; 計時不可能是百分百精確的, 即使設定為0, 也不是百分百精確的, 所以可以設定合理的允許誤差, 單位: 納秒(NSEC_PER_SEC)


相關內容, 可參考文章: Dispatch Source Timer 的使用以及注意事項

// 重複執行的定時器
- (void)gcdTimer1 {
    
    // 獲取全域性佇列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    // 建立定時器
    dispatch_source_t _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    
    // 開始時間
    dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
    
//    dispatch_time_t start = dispatch_walltime(NULL, 0);
    
    // 重複間隔
    uint64_t interval = (uint64_t)(1.0 * NSEC_PER_SEC);
    
    // 設定定時器
    dispatch_source_set_timer(_timer, start, interval, 0);
    
    // 設定需要執行的事件
    dispatch_source_set_event_handler(_timer, ^{
        
        //在這裡執行事件
        static NSInteger num = 0;
        
        NSLog(@"%ld", (long)num);
        num++;
        
        if (num > 4) {
            
            NSLog(@"end");
            
            // 關閉定時器
            dispatch_source_cancel(_timer);
        }
    });
    // 開啟定時器
    dispatch_resume(_timer);
    
    NSLog(@"start");
}

輸出:

2016-12-30 10:15:01.114 定時器[3393:99474] start
2016-12-30 10:15:02.187 定時器[3393:99796] 0
2016-12-30 10:15:03.114 定時器[3393:99796] 1
2016-12-30 10:15:04.186 定時器[3393:99796] 2
2016-12-30 10:15:05.188 定時器[3393:99796] 3
2016-12-30 10:15:06.188 定時器[3393:99796] 4
2016-12-30 10:15:06.188 定時器[3393:99796] end

這裡的開始時間設定了1s的間隔, 所以1s之後才開始執行,可以設定使用DISPATCH_TIME_NOW來立馬執行;

注意:
這裡的開始時間(start)可以使用下面的方式的來設定:

dispatch_time_t start = dispatch_walltime(NULL, 0);

或者直接設定為: DISPATCH_TIME_NOW


關於 dispatch_walltime dispatch_time 的區別, 上面也有提及,也可參考stackOverflow上的這個回答; 主要區別就是前者在系統休眠時還會繼續計時, 而後者在系統休眠時就停止計時, 待系統重新啟用時, 接著繼續計時;

停止計時器:
停止GCD定時器的方式, Dispatch Source Timer 的使用以及注意事項中有提及, 主要有以下兩種:

// 關閉定時器
// 完全銷燬定時器, 重新開啟的話需要重新建立
// 全域性變數, 關閉後需要置為nil
dispatch_source_cancel(_timer);
            
// 暫停定時器
// 可使用dispatch_resume(_timer)再次開啟
// 全域性變數, 暫停後不能置為nil, 否則不能重新開啟
dispatch_suspend(_timer);

三. CADisplayLink

CADisplayLink預設每秒執行60次,通過它的 frameInterval 屬性改變每秒執行幀數,如設定為2,意味CADisplayLink每隔一幀執行一次,有效的邏輯每秒執行30

螢幕重新整理時呼叫:CADisplayLink是一個能讓我們以和螢幕重新整理率同步的頻率將特定的內容畫到螢幕上的定時器類。CADisplayLink以特定模式註冊到runloop後,每當螢幕顯示內容重新整理結束的時候,runloop就會向CADisplayLink指定的target傳送一次指定的selector訊息, CADisplayLink類對應的selector就會被呼叫一次。所以通常情況下,按照iOS裝置螢幕的重新整理率60次/秒

延遲:iOS裝置的螢幕重新整理頻率是固定的,CADisplayLink在正常情況下會在每次重新整理結束都被呼叫,精確度相當高。但如果呼叫的方法比較耗時,超過了螢幕重新整理週期,就會導致跳過若干次回撥呼叫機會。

如果CPU過於繁忙,無法保證螢幕60次/秒的重新整理率,就會導致跳過若干次呼叫回撥方法的機會,跳過次數取決CPU的忙碌程度。

使用場景:從原理上可以看出,CADisplayLink適合做介面的不停重繪,比如視訊播放的時候需要不停地獲取下一幀用於介面渲染。

+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel

引數:

  • target: 呼叫者

  • sel: 執行的方法

示例:

- (void) displayLink {
    
    CADisplayLink *display = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayRun:)];
    
    // 大概1s執行一次
// 取值範圍 1--100, 值越大, 頻率越高
    display.preferredFramesPerSecond = 2;
    
    [display addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)displayRun:(CADisplayLink *)link {
    
    static NSInteger num = 0;
    
    NSLog(@"%ld", (long)num);
    num++;
    
    if (num > 4) {
        
        [link invalidate];
        
        NSLog(@"end");
    }
}

這裡的示例不太恰當, 不應該在這種場合使用,
另外, 我們可以使用他的 paused 屬性, 來使其暫停, 或繼續:

// 暫停
    display.paused = YES;
// 繼續
    display.paused = NO;

相關文章