NSTimer你真的會用了嗎html
看到這個標題,你可能會想NSTimer不就是計時器嗎,誰不會用,不就是一個可以定時的完成任務的東西嗎?多線程
我想說你知道NSTimer會retain你添加調用方法的對象嗎?你知道NSTimer是要加到runloop中才會起做用嗎?你知道NSTimer會並非準確的按照你指定的時間觸發的嗎?你知道NSTimer就算添加到runloop了也不必定會按照你想象中的那樣執行嗎?app
若是上面提出的哪些問題,你並不所有了解,那麼請細心的看完下面的文章,上面的那幾個問題我會一一說明,並給出詳細的例子。框架
1、什麼是NSTimeride
官方給出解釋是「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就是一個能在從如今開始的後面的某一個時刻或者週期性的執行咱們指定的方法的對象。函數
2、NSTimer和它調用的函數對象間到底發生了什麼oop
從前面官方給出的解釋能夠看出timer會在將來的某個時刻執行一次或者屢次咱們指定的方法,這也就牽扯出一個問題,如何保證timer在將來的某個時刻觸發指定事件的時候,咱們指定的方法是有效的呢?測試
解決方法很簡單,只要將指定給timer的方法的接收者retain一份就搞定了,實際上系統也是這樣作的。不論是重複性的timer仍是一次性的timer都會對它的方法的接收者進行retain,這兩種timer的區別在於「一次性的timer在完成調用之後會自動將本身invalidate,而重複的timer則將永生,直到你顯示的invalidate它爲止」。大數據
下面咱們看個小例子:spa
SvTestObject.m
SvTestObject.h
SvTimerAppDelegate.m
上面的簡單例子中,咱們自定義了一個繼承自NSObject的類SvTestObject,在這個類的init,dealloc和它的timerAction三個方法中分別打印信息。而後在appDelegate中分別測試一個單次執行的timer和一個重複執行的timer對方法接受者是否作了retain操做,所以咱們在兩種狀況下都是shedule完timer以後立馬對該測試對象執行release操做。
測試單次執行的timer的結果以下:
觀察輸出,咱們會發現53分58秒的時候咱們就對測試對象執行了release操做,可是知道54分03秒的時候timer觸發完方法之後,該對象才實際的執行了dealloc方法。這就證實一次性的timer也會retain它的方法接收者,直到本身失效爲之。
測試重複性的timer的結果以下:
觀察輸出咱們發現,這個重複性的timer一直都在週期性的調用咱們爲它指定的方法,並且測試的對象也一直沒有真正的被釋放。
經過以上小例子,咱們能夠發如今timer對它的接收者進行retain,從而保證了timer調用時的正確性,可是又引入了接收者的內存管理問題。特別是對於重複性的timer,它所引用的對象將一直存在,將會形成內存泄露。
有問題就有應對方法,NSTimer提供了一個方法invalidate,讓咱們能夠解決這種問題。不論是一次性的仍是重複性的timer,在執行完invalidate之後都會變成無效,所以對於重複性的timer咱們必定要有對應的invalidate。
忽然想起一種自欺欺人的寫法,不知道大家有沒有這麼寫過,我認可以前也有這樣寫過,哈哈,代碼以下:
SvCheatYourself.m
綜上: timer都會對它的target進行retain,咱們須要當心對待這個target的生命週期問題,尤爲是重複性的timer。
3、NSTimer會是準時觸發事件嗎
答案是否認的,並且有時候你會發現實際的觸發時間跟你想象的差距還比較大。NSTimer不是一個實時系統,所以不論是一次性的仍是週期性的timer的實際觸發事件的時間可能都會跟咱們預想的會有出入。差距的大小跟當前咱們程序的執行狀況有關係,好比可能程序是多線程的,而你的timer只是添加在某一個線程的runloop的某一種指定的runloopmode中,因爲多線程一般都是分時執行的,並且每次執行的mode也可能隨着實際狀況發生變化。
假設你添加了一個timer指定2秒後觸發某一個事件,可是簽好那個時候當前線程在執行一個連續運算(例如大數據塊的處理等),這個時候timer就會延遲到該連續運算執行完之後纔會執行。重複性的timer遇到這種狀況,若是延遲超過了一個週期,則會和後面的觸發進行合併,即在一個週期內只會觸發一次。可是無論該timer的觸發時間延遲的有多離譜,他後面的timer的觸發時間老是倍數於第一次添加timer的間隙。
原文以下「A repeating timer reschedules itself based on the scheduled firing time, not the actual firing time. For example, if a timer is scheduled to fire at a particular time and every 5 seconds after that, the scheduled firing time will always fall on the original 5 second time intervals, even if the actual firing time gets delayed. If the firing time is delayed so far that it passes one or more of the scheduled firing times, the timer is fired only once for that time period; the timer is then rescheduled, after firing, for the next scheduled firing time in the future.」
下面請看一個簡單的例子:
Simulate Thread Busy
例子中首先開啓了一個timer,這個timer每隔1秒調用一次target的timerAction方法,緊接着咱們在3秒後調用了一個模擬線程繁忙的方法(其實就是一個大的循環)。運行程序後輸出結果以下:
觀察結果咱們能夠發現,當線程空閒的時候timer的消息觸發仍是比較準確的,可是在36分12秒開始線程一直忙着作大量運算,知道36分14秒該運算才結束,這個時候timer才觸發消息,這個線程繁忙的過程超過了一個週期,可是timer並無連着觸發兩次消息,而只是觸發了一次。等線程忙完之後後面的消息觸發的時間仍然都是整數倍與開始咱們指定的時間,這也從側面證實,timer並不會由於觸發延遲而致使後面的觸發時間發生延遲。
綜上: timer不是一種實時的機制,會存在延遲,並且延遲的程度跟當前線程的執行狀況有關。
4、NSTimer爲何要添加到RunLoop中才會有做用
前面的例子中咱們使用的是一種便利方法,它實際上是作了兩件事:首先建立一個timer,而後將該timer添加到當前runloop的default mode中。也就是這個便利方法給咱們形成了只要建立了timer就能夠生效的錯覺,咱們固然能夠本身建立timer,而後手動的把它添加到指定runloop的指定mode中去。
NSTimer其實也是一種資源,若是看過多線程變成指引文檔的話,咱們會發現全部的source若是要起做用,就得加到runloop中去。同理timer這種資源要想起做用,那確定也須要加到runloop中才會又效嘍。若是一個runloop裏面不包含任何資源的話,運行該runloop時會立馬退出。你可能會說那咱們APP的主線程的runloop咱們沒有往其中添加任何資源,爲何它還好好的運行。咱們不添加,不表明框架沒有添加,若是有興趣的話你能夠打印一下main thread的runloop,你會發現有不少資源。
下面咱們看一個小例子:
- (void)applicationDidBecomeActive:(UIApplication *)application { [self testTimerWithOutShedule]; } - (void)testTimerWithOutShedule { NSLog(@"Test timer without shedult to runloop"); SvTestObject *testObject3 = [[SvTestObject alloc] init]; NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:1] interval:1 target:testObject3 selector:@selector(timerAction:) userInfo:nil repeats:NO]; [testObject3 release]; NSLog(@"invoke release to testObject3"); } - (void)applicationWillResignActive:(UIApplication *)application { NSLog(@"SvTimerSample Will resign Avtive!"); }
這個小例子中咱們新建了一個timer,爲它指定了有效的target和selector,並指出了1秒後觸發該消息,運行結果以下:
觀察發現這個消息永遠也不會觸發,緣由很簡單,咱們沒有將timer添加到runloop中。
綜上: 必須得把timer添加到runloop中,它纔會生效。
5、NSTimer加到了RunLoop中但遲遲的不觸發事件
爲何明明添加了,可是就是不按照預先的邏輯觸發事件呢???緣由主要有如下兩個:
一、runloop是否運行
每個線程都有它本身的runloop,程序的主線程會自動的使runloop生效,但對於咱們本身新建的線程,它的runloop是不會本身運行起來,當咱們須要使用它的runloop時,就得本身啓動。
那麼若是咱們把一個timer添加到了非主線的runloop中,它還會按照預期按時觸發嗎?下面請看一段測試程序:
- (void)applicationDidBecomeActive:(UIApplication *)application { [NSThread detachNewThreadSelector:@selector(testTimerSheduleToRunloop1) toTarget:self withObject:nil]; } // 測試把timer加到不運行的runloop上的狀況 - (void)testTimerSheduleToRunloop1 { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; NSLog(@"Test timer shedult to a non-running runloop"); SvTestObject *testObject4 = [[SvTestObject alloc] init]; NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:1] interval:1 target:testObject4 selector:@selector(timerAction:) userInfo:nil repeats:NO]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; // 打開下面一行輸出runloop的內容就能夠看出,timer倒是已經被添加進去 //NSLog(@"the thread's runloop: %@", [NSRunLoop currentRunLoop]); // 打開下面一行, 該線程的runloop就會運行起來,timer纔會起做用 //[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]]; [testObject4 release]; NSLog(@"invoke release to testObject4"); [pool release]; } - (void)applicationWillResignActive:(UIApplication *)application { NSLog(@"SvTimerSample Will resign Avtive!"); }
上面的程序中,咱們新建立了一個線程,而後建立一個timer,並把它添加當該線程的runloop當中,可是運行結果以下:
觀察運行結果,咱們發現這個timer知道執行退出也沒有觸發咱們指定的方法,若是咱們把上面測試程序中「//[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];」這一行的註釋去掉,則timer將會正確的掉用咱們指定的方法。
二、mode是否正確
咱們前面本身動手添加runloop的時候,能夠看到有一個參數runloopMode,這個參數是幹嗎的呢?
前面提到了要想timer生效,咱們就得把它添加到指定runloop的指定mode中去,一般是主線程的defalut mode。但有時咱們這樣作了,卻仍然發現timer仍是沒有觸發事件。這是爲何呢?
這是由於timer添加的時候,咱們須要指定一個mode,由於同一線程的runloop在運行的時候,任意時刻只能處於一種mode。因此只能當程序處於這種mode的時候,timer才能獲得觸發事件的機會。
舉個不恰當的例子,咱們說兄弟幾個分別表明runloop的mode,timer表明他們本身的才水桶,而後一羣人去排隊打水,只有一個水龍頭,那麼同一時刻,確定只能有一我的處於接水的狀態。也就是說你雖然給了老二一個桶,可是還沒輪到它,那麼你就得等,只有輪到他的時候你的水桶才能碰上用場。
最後一個例子我就不貼了,也很簡單,須要的話,我qq發給你。
綜上: 要讓timer生效,必須保證該線程的runloop已啓動,並且其運行的runloopmode也要匹配。
不少知識咱們都覺得本身學會了,但每每對細節瞭解的還不是很清楚,這也就埋了一些隱患,致使一些奇奇怪怪的bug。因此說平時項目中要注意細節,有不清楚的地方就多查查資料,今天寫的這些官方文檔中都有,但願你們能一塊兒進步。
哦對了,還有一個小時就該到末日了,哈哈,不想寫什麼總結之類的,人活着開心就好。
注:歡迎轉載,轉載請註明出處。同時歡迎加我qq,期待與你一塊兒探討更多問題。