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 拖動時也不影響的話,有兩種解決方法
-
timer分別添加到 UITrackingRunLoopMode 和 NSDefaultRunLoopMode 中
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; [[NSRunLoop mainRunLoop] addTimer:timer forMode: UITrackingRunLoopMode];
-
直接將 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);
上面代碼中要注意的是:
- dispatch_source_set_event_handler() 中的任務實在子線程中執行的,若需要回到主線程,要調用 dispatch_async(dispatch_get_main_queue(), ^{} .
- dispatch_source_set_timer 中第二個參數,當我們使用 dispatch_time 或者 DISPATCH_TIME_NOW 時,系統會使用默認時鐘來進行計時。然而當系統休眠的時候,默認時鐘是不走的,也就會導致計時器停止。使用 dispatch_walltime 可以讓計時器按照真實時間間隔進行計時.
- 第三個參數, 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