iOS定時器,你真的會使用嗎?

kkiw1274 7年前發布 | 22K 次閱讀 RunLoop iOS開發 移動開發

定時器的使用是軟件開發基礎技能,用于延時執行或重復執行某些方法。我相信大部分人接觸iOS的定時器都是從

[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(action:) userInfo:nil repeats:YES]這段代碼開始的吧。但是你真的會用嗎?

iOS定時器

首先來介紹iOS中的定時器

iOS中的定時器大致分為這幾類:

  • NSTimer
  • CADisplayLink
  • GCD定時器

NSTimer

使用方法

NSTime定時器是我們比較常使用的定時器,比較常使用的方法有兩種:

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

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

這兩種方法都是創建一個定時器,區別是用 timerWithTimeInterval: 方法創建的定時器需要手動加入RunLoop中。

// 創建NSTimer對象
NSTimer *timer = [NSTimer timerWithTimeInterval:3 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
// 加入RunLoop中
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

需要 注意 的是: UIScrollView 滑動時執行的是 UITrackingRunLoopMode , NSDefaultRunLoopMode 被掛起,會導致定時器失效,等恢復為 滑動結束 時才恢復定時器。

舉個例子:

- (void)startTimer{
    NSTimer *UIScrollView = [NSTimer timerWithTimeInterval:0.5 target:self selector:@selector(action:) userInfo:nil repeats:YES];
    [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
}

- (void)action:(NSTimer *)sender {
    static int i = 0;
    NSLog(@"NSTimer: %d",i);
    i++;
}

將 timer 添加到 NSDefaultRunLoopMode 中,沒0.5秒打印一次,然后滑動 UIScrollView .

打印臺輸出:

可以看出在滑動 UIScrollView 時,定時器被暫停了。

所以如果需要定時器在 UIScrollView 拖動時也不影響的話,有兩種解決方法

  1. timer分別添加到 UITrackingRunLoopMode 和 NSDefaultRunLoopMode 中

    [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    [[NSRunLoop mainRunLoop] addTimer:timer forMode: UITrackingRunLoopMode];
  2. 直接將 timer 添加到 NSRunLoopCommonModes 中:

    [[NSRunLoop mainRunLoop] addTimer:timer forMode: NSRunLoopCommonModes];

但并不是都 timer 所有的需要在滑動 UIScrollView 時繼續執行,比如使用 NSTimer 完成的幀動畫,滑動 UIScrollView 時就可以停止幀動畫,保證滑動的流程性。

若沒有特殊要求的話,一般使用第二種方法創建完 timer ,會自動添加到 NSDefaultRunLoopMode 中去執行,也是平時最常用的方法。

NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(action:) userInfo:nil repeats:YES];

參數:

TimeInterval :延時時間

target :目標對象,一般就是 self 本身

selector :執行方法

userInfo :傳入信息

repeats :是否重復執行

以上創建的定時器,若 repeats 參數設為 NO ,執行一次后就會被釋放掉;

若 repeats 參數設為 YES 重復執行時,必須手動關閉,否則定時器不會釋放(停止)。

釋放方法:

// 停止定時器
[timer invalidate];

實際開發中,我們會將 NSTimer 對象設置為屬性,這樣方便釋放。

iOS10.0推出了兩個新的API,與上面的方法相比, selector 換成Block回調以、減少傳入的參數(那幾個參數真是雞肋)。不過開發中一般需要適配低版本,還是盡量使用上面的方法吧。

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

特點

  • 必須加入Runloop

    上面不管使用哪種方法,實際最后都會加入RunLoop中執行,區別就在于是否手動加入而已。

  • 存在延遲

    不管是一次性的還是周期性的timer的實際觸發事件的時間,都會與所加入的RunLoop和RunLoop Mode有關,如果此RunLoop正在執行一個連續性的運算,timer就會被延時出發。重復性的timer遇到這種情況,如果延遲超過了一個周期,則會在延時結束后立刻執行,并按照之前指定的周期繼續執行,這個延遲時間大概為50-100毫秒.

    所以NSTimer不是絕對準確的,而且中間耗時或阻塞錯過下一個點,那么下一個點就pass過去了.

  • UIScrollView滑動會暫停計時

    添加到 NSDefaultRunLoopMode 的 timer 在 UIScrollView 滑動時會暫停,若不想被 UIScrollView 滑動影響,需要將 timer 添加再到 UITrackingRunLoopMode 或 直接添加到 NSRunLoopCommonModes 中

CADisplayLink

CADisplayLink官方介紹:

A CADisplayLink object is a timer object that allows your application to synchronize its drawing to the refresh rate of the display

CADisplayLink對象是一個和屏幕刷新率同步的定時器對象。每當屏幕顯示內容刷新結束的時候,runloop就會向CADisplayLink指定的 target 發送一次指定的 selector 消息, CADisplayLink類對應的 selector 就會被調用一次。

從原理上可以看出,CADisplayLink適合做界面的不停重繪,比如視頻播放的時候需要不停地獲取下一幀用于界面渲染,或者做動畫。

使用方法

創建:

@property (nonatomic, strong) CADisplayLink *displayLink;

self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDisplayLink:)];  

// 每隔1幀調用一次
self.displayLink.frameInterval = 1;  

[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

釋放方法:

[self.displayLink invalidate];  

self.displayLink = nil;

當把 CADisplayLink 對象添加到runloop中后, selector 就能被周期性調用,類似于重復的NSTimer被啟動了;執行 invalidate 操作時,CADisplayLink對象就會從runloop中移除, selector 調用也隨即停止,類似于NSTimer的 invalidate 方法。

CADisplayLink中有兩個重要的屬性:

  • frameInterval

    NSInteger類型的值,用來設置間隔多少幀調用一次 selector 方法,默認值是1,即每幀都調用一次。

  • duration

    CFTimeInterval 值為 readOnly ,表示兩次屏幕刷新之間的時間間隔。需要注意的是,該屬性在 targe t的 selector 被首次調用以后才會被賦值。 selector 的調用間隔時間計算方式是: 調用間隔時間 = duration × frameInterval

特點

  • 刷新頻率固定

    正常情況iOS設備的屏幕刷新頻率是固定 60Hz ,如果CPU過于繁忙,無法保證屏幕60次/秒的刷新率,就會導致跳過若干次調用回調方法的機會,跳過次數取決CPU的忙碌程度。

  • 屏幕刷新時調用

    CADisplayLink在正常情況下會在每次刷新結束都被調用,精確度相當高。但如果調用的方法比較耗時,超過了屏幕刷新周期,就會導致跳過若干次回調調用機會

  • 適合做界面渲染

    CADisplayLink可以確保系統渲染每一幀的時候我們的方法都被調用,從而保證了動畫的流暢性。

GCD定時器

GCD定時器和NSTimer是不一樣的,NSTimer受RunLoop影響,但是GCD的定時器不受影響,因為通過源碼可知RunLoop也是基于GCD的實現的,所以GCD定時器有非常高的精度。

使用方法

創建GCD定時器定時器的方法稍微比較復雜,看下面的代碼:

單次的延時調用

NSObject中的 performSelector:withObject:afterDelay: 以及 performSelector:withObject:afterDelay:inModes: 這兩個方法在調用的時候會設置當前 runloop 中 timer ,前者設置的 timer 在 NSDefaultRunLoopMode 運行,后者則可以指定 NSRunLoop 的 mode 來執行。我們上面介紹過 runloop 中 timer 在 UITrackingRunLoopMode 被掛起,就導致了代碼就會一直等待 timer 的調度,解決辦法在上面也有說明。

不過我們可以用另一套方案來解決這個問題,就是使用GCD中的 dispatch_after 來實現單次的延時調用:

double delayInSeconds = 2.0;
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
        [self someMethod];
    });

循環調用

// 創建GCD定時器
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_source_set_timer(_timer, dispatch_walltime(NULL, 0), 1.0 * NSEC_PER_SEC, 0); //每秒執行

// 事件回調
dispatch_source_set_event_handler(_timer, ^{

    dispatch_async(dispatch_get_main_queue(), ^{
        // 在主線程中實現需要的功能

    }
}

});

// 開啟定時器
dispatch_resume(_timer);

// 掛起定時器(dispatch_suspend 之后的 Timer,是不能被釋放的!會引起崩潰)
dispatch_suspend(_timer);

// 關閉定時器
dispatch_source_cancel(_timer);

上面代碼中要注意的是:

  1. dispatch_source_set_event_handler() 中的任務實在子線程中執行的,若需要回到主線程,要調用 dispatch_async(dispatch_get_main_queue(), ^{} .
  2. dispatch_source_set_timer 中第二個參數,當我們使用 dispatch_time 或者 DISPATCH_TIME_NOW 時,系統會使用默認時鐘來進行計時。然而當系統休眠的時候,默認時鐘是不走的,也就會導致計時器停止。使用 dispatch_walltime 可以讓計時器按照真實時間間隔進行計時.
  3. 第三個參數, 1.0 * NSEC_PER_SEC 為每秒執行一次,對應的還有毫秒,分秒,納秒可以選擇.
  • dispatch_source_set_event_handler 這個函數在執行完之后,block 會立馬執行一遍,后面隔一定時間間隔再執行一次。而 NSTimer 第一次執行是到計時器觸發之后。這也是和 NSTimer 之間的一個顯著區別。
  • 掛起(暫停)定時器, dispatch_suspend 之后的 Timer ,不能被釋放的,會引起崩潰.
  • 創建的 timer 一定要有 dispatch_suspend(_timer) 或 dispatch_source_cancel(_timer) 這兩句話來指定出口,否則定時器將不執行,若我們想無限循環可將 dispatch_source_cancel(_timer) 寫在一句永不執行的 if 判斷語句中。

使用場景

介紹完iOS中的各種定時器,接下來我們來說說這幾種定時器在開發中的幾種用法。

短信重發倒計時

短信倒計時使我們登錄注冊常用的功能,一般設置為60s,實現方法如下:

// 計時時間
@property (nonatomic, assign) int timeout;

/** 開啟倒計時 */
- (void)startCountdown {

    if (_timeout > 0) {
        return;
    }

    _timeout = 60;

    // GCD定時器
    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_source_set_timer(_timer, dispatch_walltime(NULL, 0), 1.0 * NSEC_PER_SEC, 0); //每秒執行

    dispatch_source_set_event_handler(_timer, ^{

        if(_timeout <= 0 ){// 倒計時結束

            // 關閉定時器
            dispatch_source_cancel(_timer);

            dispatch_async(dispatch_get_main_queue(), ^{

                //設置界面的按鈕顯示 根據自己需求設置
                [self.sendMsgBtn setTitle:@"發送" forState:UIControlStateNormal];

                self.sendMsgBtn.enabled = YES;

            });

        }else{// 倒計時中

            // 顯示倒計時結果

            NSString *strTime = [NSString stringWithFormat:@"重發(%.2d)", _timeout];

            dispatch_async(dispatch_get_main_queue(), ^{

                //設置界面的按鈕顯示 根據自己需求設置

                [self.sendMsgBtn setTitle:[NSString stringWithFormat:@"%@",strTime] forState:UIControlStateNormal];

                self.sendMsgBtn.enabled = NO;

            });

            _timeout--;
        }
    });

    // 開啟定時器
    dispatch_resume(_timer);

}

在上面代碼中,我們設置了一個60s循環倒計時,當我們向服務器獲取短信驗證碼成功時 調用該方法開始倒計時。每秒刷新按鈕的倒計時數,倒計時結束時再將按鈕 Title 恢復為“發送”.

有一點需要注意的是,按鈕的樣式要設置為 UIButtonTypeCustom ,否則會出現刷新 Title 時閃爍.

我們可以把這個方法封裝一下,方便調用,否則在控制器中寫這么一大段代碼確實也不優雅。

效果如下:

 

每個幾分鐘向服務器發送數據

在有定位服務的APP中,我們需要每個一段時間將定位數據發送到服務器,比如每5s定位一次每隔5分鐘將再統一將數據發送服務器,這樣會處理比較省電。

一般程序進入后臺時,定時器會停止,但是在定位APP中,需要持續進行定位,APP在后臺時依舊可以運行,所以在后臺定時器也是可以運行的。

在使用GCD定時的時候發現GCD定時器也可以在后代運行,創建方法同上面的短信倒計時.

這里我們使用 NSTimer 來創建一個每個5分鐘執行一次的定時器.

#import <Foundation/Foundation.h>

typedef void(^TimerBlock)();

@interface BYTimer : NSObject

- (void)startTimerWithBlock:(TimerBlock)timerBlock;

- (void)stopTimer;

@end
#import "BYTimer.h"

@interface BYTimer ()

@property (nonatomic, strong) NSTimer *timer;
@property (nonatomic, strong) TimerBlock timerBlock;

@end

@implementation BYTimer

- (void)startTimerWithBlock:(TimerBlock)timerBlock {

     self.timer = [NSTimer timerWithTimeInterval:300 target:self selector:@selector(_timerAction) userInfo:nil repeats:YES];

    [[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
    _timerBlock = timerBlock;

}

- (void)_timerAction {
    if (self.timerBlock) {
        self.timerBlock();
    }
}

- (void)stopTimer {
    [self.timer invalidate];
}

@end

該接口的實現很簡單,就是 NSTimer 創建了一個300s執行一次的定時器,但是要注意定時器需要加入 NSRunLoopCommonModes 中。

 

 

來自:http://www.jianshu.com/p/c167ca4d1e7e

 

 本文由用戶 kkiw1274 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!