一 : 什麼是NSTimer?
官方文檔說“A timer provides a way to perform a delayed action or a periodic action. The timer waits until a certain time interval has elapsed and then fires, sending a specified message to a specified object. ” 意思就是timer就是一個能在從現在開始的後面的某一個時刻或者周期性的執行我們指定的方法的對象。從官方給出的解釋可以看出timer會在未來的某個時刻執行一次或者多次我們指定的方法,這也就牽扯出一個問題,如何保證timer在未來的某個時刻觸發指定事件的時候,我們指定的方法是有效的呢?
解決方法很簡單,隻要将指定給timer的方法的接收者retain一份就搞定了,實際上系統也是這樣做的。不管是重複性的timer還是一次性的timer都會對它的方法的接收者進行retain,這兩種timer的差別在于“一次性的timer在完成調用以後會自動将自己invalidate,而重複的timer則将永生,直到你顯示的invalidate它為止”。
二 :NSTimer和NSRunLoop的關系?
隻要出現NSTimer必須要有NSRunLoop,NSTimer必須依賴NSRunLoop才能執行 。NSTimer其實也是一種資源,如果看過多線程程式設計指引文檔的話,我們會發現所有的source如果要起作用,就得加到runloop中去。同理timer這種資源要想起作用,那肯定也需要加到runloop中才會生效喽。如果一個runloop裡面不包含任何資源的話,運作該runloop時會立馬退出。你可能會說那我們APP的主線程的runloop我們沒有往其中添加任何資源,為什麼它還好好的運作。我們不添加,不代表架構沒有添加,如果有興趣的話你可以列印一下main thread的runloop,你會發現有很多資源。
三 :NSTimer主要方法一覽
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo; //生成timer但不執行
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo; //生成timer并且納入目前線程的run loop來執行,不需要用addTimer方法。
- //關閉 - 永久關閉
- [timer invalidate];
- //暫時定時器
- [myTimer setFireDate:[NSDate distantFuture]];
- //開啟定時器
- [myTimer setFireDate:[NSDate distantPast]];
NSRunLoop與timer有關方法為:
- (void)addTimer:(NSTimer *)timer forMode:(NSString *)mode; //在run loop上注冊timer
主線程已經有run loop,是以NSTimer一般在主線程上運作都不必再調用addTimer:。但在非主線程上運作必須配置run loop
四 : NSRunLoop主要方法一覽
+ (NSRunLoop *)currentRunLoop; //獲得目前線程的run loop
+ (NSRunLoop *)mainRunLoop; //獲得主線程的run loop
- (void)run; //進入處理事件循環,如果沒有事件則立刻傳回。注意:主線程上調用這個方法會導緻無法傳回(進入無限循環,雖然不會阻塞主線程),因為主線程一般總是會有事件處理。
- (void)runUntilDate:(NSDate *)limitDate; //同run方法,增加逾時參數limitDate,避免進入無限循環。使用在UI線程(亦即主線程)上,可以達到暫停的效果。
- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate; //等待消息處理,好比在PC終端視窗上等待鍵盤輸入。一旦有合适事件(mode相當于定義了事件的類型)被處理了,則立刻傳回;類同run方法,如果沒有事件處理也立刻傳回;有否事件處理由傳回布爾值判斷。同樣limitDate為逾時參數。
- (void)acceptInputForMode:(NSString *)mode beforeDate:(NSDate *)limitDate; //似乎和runMode:差不多(測試過是這種結果,但确定是否有其它特殊情況下的不同),沒有BOOL傳回值。
官網文檔也提到run和runUntilDate:會以NSDefaultRunLoopMode參數調用runMode:來處理事件。
當app運作後,iOS系統已經幫助主線程啟動一個run loop,而一般線程則需要手動來啟動run loop。
使用run loop的一個好處就是避免線程輪詢的開銷,run loop在無事件處理時可以自動進入睡眠狀态,降低CPU的能耗。
[[NSRunLoop mainRunLoop] run]; //主線程永遠等待,但讓出主線程時間片
[[NSRunLoop mainRunLoop] runUntilDate:[NSDate distantFuture]]; //等同上面調用
[[NSRunLoop mainRunLoop] runUntilDate:[NSDate date]]; //立即傳回
[[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:10.0]]; //主線程等待,但讓出主線程時間片,然後過10秒後傳回
[[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate: [NSDate distantFuture]]; //主線程等待,但讓出主線程時間片;有事件到達就傳回,比如點選UI等。
[[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate: [NSDate date]]; //立即傳回
[[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate: [NSDate dateWithTimeIntervalSinceNow:10.0]]; //主線程等待,但讓出主線程時間片;有事件到達就傳回,如果沒有則過10秒傳回。
五 : 注意事項
我們通常在主線程中使用NSTimer,有個實際遇到的問題需要注意。當滑動界面時,系統為了更好地處理UI事件和滾動顯示,主線程runloop會暫時停止處理一些其它事件,這時主線程中運作的NSTimer就會被暫停。解決辦法就是改變NSTimer運作的mode(mode可以看成事件類型),不使用預設的NSDefaultRunLoopMode,而是改用NSRunLoopCommonModes,這樣主線程就會繼續處理NSTimer事件了。具體代碼如下:
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timer:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
//建立一個定時器
_timer=[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(changeTimeAtTimedisplay) userInfo:nil repeats:YES];
_timer=[NSTimer timerWithTimeInterval:10 target:self selector:@selector(changeTimeAtTimedisplay) userInfo:nil repeats:YES];
//必須手動加入到目前循環中去
NSRunLoop *runloop=[NSRunLoop currentRunLoop];
[runloop addTimer:_timer forMode:NSDefaultRunLoopMode];
以上兩端代碼效果是一樣的
六,NSTimer一定準确麼?
NSTimer其實并不是一個實時的系統,正常情況下它能按照指定的周期觸發,但如果目前線程有阻塞的時候會延遲執行,在延遲超過一個周期時會和下一個觸發合并在下一個觸發時刻執行。除此之外,多線程程式實際上也是要在CPU的處理上同步進行,作業系統并不能保證多線程嚴格同步。一個很典型的場景就是:如果我們定義一個一秒周期的定時器,希望它保持一秒計數,當計時的時間越來越長的時候,誤差會越來越大。
七,如何在使NSTimer在背景也能執行?
正常情況下,NSTimer會在應用進入背景時停止工作,進入前台時又重新計時。那麼怎麼使NSTimer在背景也能執行呢?
要完成這個需求,就要借助蘋果上的音頻播放類在背景執行的這個特權。具體操作方法如下:
在Info.plist中,添加"Required background modes"數組鍵,設定一個元素為"App plays audio".
在- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions方法中添加:
NSError *err = nil;
[[AVAudioSession sharedInstance]setCategory: AVAudioSessionCategoryPlayback error: &err];
[[AVAudioSession sharedInstance]setActive: YES error: &err];
再添加如下方法:
折疊C/C++ Code複制内容到剪貼闆
- - (void)applicationDidEnterBackground:(UIApplication *)application{
- UIApplication* app = [UIApplication sharedApplication];
- __block UIBackgroundTaskIdentifier bgTask;
- bgTask = [app beginBackgroundTaskWithExpirationHandler:^{
- dispatch_async(dispatch_get_main_queue(), ^{
- if (bgTask != UIBackgroundTaskInvalid)
- {
- bgTask = UIBackgroundTaskInvalid;
- }
- });
- }];
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
- dispatch_async(dispatch_get_main_queue(), ^{
- if (bgTask != UIBackgroundTaskInvalid)
- {
- bgTask = UIBackgroundTaskInvalid;
- }
- });
- });
- }
還有一種犧牲頁面流暢性的方法,直接在主線程中,提高timer的runloop權限,不過建議為了使用者體驗,還是放棄這種方法。
if (nil == self.updateTimer)
{
self.updateTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(updateTime) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.updateTimer forMode:NSRunLoopCommonModes];
}
八,關于這個方法:
Firing a Timer
– fire
其實他并不是真的啟動一個定時器,從之前的初始化方法中我們也可以看到,建立的時候,在适當的時間,定時器就會自動啟動。那這個方法是幹什麼的呢。
You can use this method to fire a repeating timer without interrupting its regular firing schedule. If the timer is non-repeating, it is automatically invalidated after firing, even if its scheduled fire date has not arrived.
這是官方文檔的說法,英文說的很清楚,但我們了解還不是很到位,為了徹底搞懂它的功能。我又做了一個測試。
也是很簡單,把上面那個定時器,改變一點
//初始化的時候建立一個定時器
- (id) initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Custom initialization
//建立一個定時器,
_timer=[NSTimer timerWithTimeInterval:10 target:self selector:@selector(changeTimeAtTimedisplay) userInfo:nil repeats:YES];
//手動加入到循環中
NSRunLoop *runloop=[NSRunLoop currentRunLoop];
[runloop addTimer:_timer forMode:NSDefaultRunLoopMode];
//當然這個定時器會自動啟動,隻不多過了十秒之後,才觸發
}
return self
}
當我們單擊“開始”按鈕時,
- (IBAction)startTime:(id)sender {
//隻是簡單地調用一下這個方法,看到底功能是什麼
[_timer fire];
}
結果是,單擊一下按鈕,倒計時減1,單擊一下減1,即它把觸發的時間給提前了,但過十秒後倒計時還會減1,即它隻是提前觸發定時器,而不影響之前的那個定時器設定的時間,就好比我們等不及要去看一場球賽,趕緊把車開快些一樣,fire的功能就像讓我們快些到球場,但卻不影響球賽開始的時間。
還記得之前那個初始化定時器時,設定的是YES嗎,當我們,改為NO時,即不讓它循環觸發時,我們此時再單擊開始按鈕。會猛然發現,倒計時減1了,但當我們再點選開始按鈕時,會發現倒計時,不會動了。原因是:我們的定時器,被設定成隻觸發一次,再fire的時候,觸發一次,該定時器,就被自動銷毀了,以後再fire也不會觸發了。
現在 我們再看官方的解釋,或許就會更明白了,
You can use this method to fire a repeating timer without interrupting its regular firing schedule. If the timer is non-repeating, it is automatically invalidated after firing, even if its scheduled fire date has not arrived.
這就對了,fire并不是啟動一個定時器,隻是提前觸發而已。