關(guān)于定時器NSTimer的深入理解

一树姨、什么是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就是一個能在從現(xiàn)在開始的后面的某一個時刻或者周期性的執(zhí)行我們指定的方法的對象摩桶。

二、NSTimer和它調(diào)用的函數(shù)對象間到底發(fā)生了什么

從前面官方給出的解釋可以看出timer會在未來的某個時刻執(zhí)行一次或者多次我們指定的方法帽揪,這也就牽扯出一個問題硝清,如何保證timer在未來的某個時刻觸發(fā)指定事件的時候,我們指定的方法是有效的呢转晰?

解決方法很簡單芦拿,只要將指定給timer的方法的接收者retain一份就搞定了,實際上系統(tǒng)也是這樣做的查邢。不管是重復(fù)性的timer還是一次性的timer都會對它的方法的接收者進(jìn)行retain蔗崎,這兩種timer的區(qū)別在于“一次性的timer在完成調(diào)用以后會自動將自己invalidate,而重復(fù)的timer則將永生扰藕,直到你顯示的invalidate它為止”缓苛。

下面我們看個小例子:

import <Foundation/Foundation.h>

@interface TestObject : NSObject

//timer響應(yīng)函數(shù),只是用來做測試
-(void)timerAction:(NSTimer *)timer;

@end

import "TestObject.h"

@implementation TestObject

-(id)init
{
self = [super init];

if(self)  
{  
    NSLog(@"instance %@ has been created!", self);  
}  
  
return self;  

}

-(void)dealloc
{
NSLog(@"instance %@ has been dealloced!", self);

[super dealloc];  

}

-(void)timerAction:(NSTimer *)timer
{
NSLog(@"Hi邓深,Timer Action for instance %@", self);
}

@end

import "ViewController.h"

import "TestObject.h"

@interface ViewController ()

@end

@implementation ViewController

  • (void)viewDidLoad
    {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    //測試NSTimer是否會retain一個Object對象未桥?答案:會
    [self testNonRepeatTimer];
    // [self testRepeatTimer];

}

//不循環(huán)的定時器測試方法
-(void)testNonRepeatTimer
{
NSLog(@"Test retain target for non-repeat");

TestObject *testObject = [[TestObject alloc] init];  
[NSTimer scheduledTimerWithTimeInterval:5 target:testObject selector:@selector(timerAction:) userInfo:nil repeats:NO];  
[testObject release];  
NSLog(@"Invoke release to testObject");  

}

//循環(huán)的定時器測試方法
-(void)testRepeatTimer
{
NSLog(@"Test retain target for repeat Timer");

TestObject *testObject2 = [[TestObject alloc] init];  
  
[NSTimer scheduledTimerWithTimeInterval:5 target:testObject2 selector:@selector(timerAction:) userInfo:nil repeats:YES];  
[testObject2 release];  
  
NSLog(@"Invoke release to testObject2");  

}

  上面的簡單例子中,我們自定義了一個繼承自NSObject的類TestObject庐完,在這個類的init钢属,dealloc和它的timerAction三個方法中分別打印信息。然后在appDelegate中分別測試一個單次執(zhí)行的timer和一個重復(fù)執(zhí)行的timer對方法接受者是否做了retain操作门躯,因此我們在兩種情況下都是shedule完timer之后立馬對該測試對象執(zhí)行release操作淆党。

測試單次執(zhí)行的timer的結(jié)果如下:

37F60DAF-A001-461F-B384-727B7710DDA3.png

觀察輸出,我們會發(fā)現(xiàn)53分58秒的時候我們就對測試對象執(zhí)行了release操作讶凉,但是知道54分03秒的時候timer觸發(fā)完方法以后染乌,該對象才實際的執(zhí)行了dealloc方法。這就證明一次性的timer也會retain它的方法接收者懂讯,直到自己失效為之荷憋。

測試重復(fù)性的timer的結(jié)果如下:


572B2320-A603-461B-AC33-25A0A346D1F5.png

觀察輸出我們發(fā)現(xiàn),這個重復(fù)性的timer一直都在周期性的調(diào)用我們?yōu)樗付ǖ姆椒ê滞覝y試的對象也一直沒有真正的被釋放勒庄。

通過以上小例子,我們可以發(fā)現(xiàn)在timer對它的接收者進(jìn)行retain瘫里,從而保證了timer調(diào)用時的正確性实蔽,但是又引入了接收者的內(nèi)存管理問題。特別是對于重復(fù)性的timer谨读,它所引用的對象將一直存在局装,將會造成內(nèi)存泄露。

有問題就有應(yīng)對方法,NSTimer提供了一個方法invalidate,讓我們可以解決這種問題。不管是一次性的還是重復(fù)性的timer辨赐,在執(zhí)行完invalidate以后都會變成無效,因此對于重復(fù)性的timer我們一定要有對應(yīng)的invalidate玫膀。

 因此你有必要看一下這篇文章咯: IOS中定時器NSTimer的開啟與關(guān)閉。

綜上: timer都會對它的target進(jìn)行retain统舀,我們需要小心對待這個target的生命周期問題匆骗,尤其是重復(fù)性的timer。
三誉简、NSTimer會是準(zhǔn)時觸發(fā)事件嗎

答案是否定的,而且有時候你會發(fā)現(xiàn)實際的觸發(fā)時間跟你想象的差距還比較大盟广。NSTimer不是一個實時系統(tǒng)闷串,因此不管是一次性的還是周期性的,timer的實際觸發(fā)事件的時間可能都會跟我們預(yù)想的會有出入筋量。差距的大小跟當(dāng)前我們程序的執(zhí)行情況有關(guān)系烹吵,比如可能程序是多線程的,而你的timer只是添加在某一個線程的runloop的某一種指定的runloopmode中桨武,由于多線程通常都是分時執(zhí)行的肋拔,而且每次執(zhí)行的mode也可能隨著實際情況發(fā)生變化。

假設(shè)你添加了一個timer指定2秒后觸發(fā)某一個事件呀酸,但是簽好那個時候當(dāng)前線程在執(zhí)行一個連續(xù)運算(例如大數(shù)據(jù)塊的處理等)凉蜂,這個時候timer就會延遲到該連續(xù)運算執(zhí)行完以后才會執(zhí)行。重復(fù)性的timer遇到這種情況性誉,如果延遲超過了一個周期窿吩,則會和后面的觸發(fā)進(jìn)行合并,即在一個周期內(nèi)只會觸發(fā)一次错览。但是不管該timer的觸發(fā)時間延遲的有多離譜纫雁,他后面的timer的觸發(fā)時間總是倍數(shù)于第一次添加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.”

下面請看一個簡單的例子:

import "ViewController.h"

import "TestObject.h"

@interface ViewController ()

@end

@implementation ViewController

  • (void)viewDidLoad
    {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    //測試NSTimer是否會準(zhǔn)時觸發(fā)倾哺?答案:不會
    TestObject *testObject = [[TestObject alloc] init];
    [NSTimer scheduledTimerWithTimeInterval:1 target:testObject selector:@selector(timerAction:) userInfo:nil repeats:YES];
    [testObject release];

    NSLog(@"simulate busy");
    [self performSelector:@selector(simulateBusy) withObject:nil afterDelay:5];

}

//模擬當(dāng)前線程正好繁忙的情況
-(void)simulateBusy
{
NSLog(@"star simulate busy");

NSUInteger caculateCount = 0x0FFFFFFF;  
CGFloat uselessValue = 0;  
for (NSUInteger i = 0; i < caculateCount; ++i)  
{  
    uselessValue = i /0.333;  
}  
  
NSLog(@"finish simulate busy");  

}

-(void)testTimerWithOutShedule
{
NSLog(@"Test timer without shedult to runloop");

TestObject *testObject3 = [[TestObject 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");  

}
例子中首先開啟了一個timer轧邪,這個timer每隔1秒調(diào)用一次target的timerAction方法,緊接著我們在3秒后調(diào)用了一個模擬線程繁忙的方法(其實就是一個大的循環(huán))羞海。運行程序后輸出結(jié)果如下:

A496FCD7-B1C6-4AAF-A2EA-7D24B1BF07A3.png

仔細(xì)觀察可以發(fā)現(xiàn):當(dāng)線程空閑的時候timer的消息觸發(fā)還是比較準(zhǔn)確的忌愚,但是在20分51秒開始線程一直忙著做大量運算,知道20分53秒該運算才結(jié)束扣猫,這個時候timer才觸發(fā)消息菜循,這個線程繁忙的過程超過了一個周期,但是timer并沒有連著觸發(fā)兩次消息,而只是觸發(fā)了一次癌幕。等線程忙完以后后面的消息觸發(fā)的時間仍然都是整數(shù)倍與開始我們指定的時間衙耕,這也從側(cè)面證明,timer并不會因為觸發(fā)延遲而導(dǎo)致后面的觸發(fā)時間發(fā)生延遲勺远。

綜上: timer不是一種實時的機(jī)制橙喘,會存在延遲,而且延遲的程度跟當(dāng)前線程的執(zhí)行情況有關(guān)胶逢。

四厅瞎、NSTimer為什么要添加到RunLoop中才會有作用

前面的例子中我們使用的是一種便利方法,它其實是做了兩件事:首先創(chuàng)建一個timer初坠,然后將該timer添加到當(dāng)前runloop的default mode中和簸。也就是這個便利方法給我們造成了只要創(chuàng)建了timer就可以生效的錯覺,我們當(dāng)然可以自己創(chuàng)建timer碟刺,然后手動的把它添加到指定runloop的指定mode中去锁保。

NSTimer其實也是一種資源,如果看過多線程編程指引文檔的話半沽,我們會發(fā)現(xiàn)所有的source如果要起作用爽柒,就得加到runloop中去。同理timer這種資源要想起作用者填,那肯定也需要加到runloop中才會生效嘍浩村。如果一個runloop里面不包含任何資源的話,運行該runloop時會立馬退出占哟。你可能會說那我們APP的主線程的runloop我們沒有往其中添加任何資源心墅,為什么它還好好的運行。我們不添加重挑,不代表框架沒有添加嗓化,如果有興趣的話你可以打印一下main thread的runloop,你會發(fā)現(xiàn)有很多資源谬哀。

下面我們看一個小例子:

import "ViewController.h"

import "TestObject.h"

@interface ViewController ()

@end

@implementation ViewController

  • (void)viewDidLoad
    {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    //測試NSTimer是否需要添加到Runloop中才有效刺覆?答案:是的
    [self testTimerWithOutShedule];

}

-(void)testTimerWithOutShedule
{
NSLog(@"Test timer without shedult to runloop");

TestObject *testObject3 = [[TestObject 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)didReceiveMemoryWarning
    {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
    }

@end
這個小例子中我們新建了一個timer,為它指定了有效的target和selector史煎,并指出了1秒后觸發(fā)該消息谦屑,運行結(jié)果如下:

7410B071-032E-47DF-86BC-3C2131C5082A.png

觀察發(fā)現(xiàn)這個消息永遠(yuǎn)也不會觸發(fā),原因很簡單篇梭,我們沒有將timer添加到runloop中氢橙。

綜上: 必須得把timer添加到runloop中,它才會生效恬偷。

五悍手、NSTimer加到了RunLoop中但遲遲的不觸發(fā)事件

為什么明明添加了,但是就是不按照預(yù)先的邏輯觸發(fā)事件呢?坦康?竣付?原因主要有以下兩個:

1、runloop是否運行

每一個線程都有它自己的runloop滞欠,程序的主線程會自動的使runloop生效古胆,但對于我們自己新建的線程,它的runloop是不會自己運行起來筛璧,當(dāng)我們需要使用它的runloop時逸绎,就得自己啟動。

那么如果我們把一個timer添加到了非主線的runloop中夭谤,它還會按照預(yù)期按時觸發(fā)嗎棺牧?下面請看一段測試程序:

import "ViewController.h"

import "TestObject.h"

@interface ViewController ()

@end

@implementation ViewController

  • (void)viewDidLoad
    {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    //另開一個新的線程,進(jìn)行測試朗儒,而非主線程陨帆。
    [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");  
  
TestObject *testObject4 = [[TestObject 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的內(nèi)容就可以看出,timer確實已經(jīng)被添加進(jìn)去  

// 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)didReceiveMemoryWarning
    {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
    }

@end

上面的程序中承二,我們新創(chuàng)建了一個線程榆鼠,然后創(chuàng)建一個timer,并把它添加當(dāng)該線程的runloop當(dāng)中亥鸠,但是運行結(jié)果如下:

8AE9B787-85C7-4B72-A8A3-6497E3A35785.png

其中妆够,第一行注釋的代碼,可以打印輸出該線程的Runloop中確實已經(jīng)有了timer資源负蚊。觀察運行結(jié)果神妹,我們發(fā)現(xiàn)這個timer知道執(zhí)行退出也沒有觸發(fā)我們指定的方法,如果我們把上面測試程序中:

[cpp] view plain copy
// [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
這一行的注釋去掉家妆,則timer將會正確的調(diào)用我們指定的方法鸵荠。

2、mode是否正確

我們前面自己動手添加runloop的時候伤极,可以看到有一個參數(shù)runloopMode蛹找,這個參數(shù)是干嘛的呢?

前面提到了要想timer生效哨坪,我們就得把它添加到指定runloop的指定mode中去庸疾,通常是主線程的defalut mode。但有時我們這樣做了当编,卻仍然發(fā)現(xiàn)timer還是沒有觸發(fā)事件届慈。這是為什么呢?

這是因為timer添加的時候,我們需要指定一個mode金顿,因為同一線程的runloop在運行的時候臊泌,任意時刻只能處于一種mode。所以只能當(dāng)程序處于這種mode的時候串绩,timer才能得到觸發(fā)事件的機(jī)會缺虐。

舉個不恰當(dāng)?shù)睦樱覀冋f兄弟幾個分別代表runloop的mode礁凡,timer代表他們自己的水桶高氮,然后一群人去排隊打水,只有一個水龍頭顷牌,那么同一時刻剪芍,肯定只能有一個人處于接水的狀態(tài)。也就是說你雖然給了老二一個桶窟蓝,但是還沒輪到它罪裹,那么你就得等,只有輪到他的時候你的水桶才能派上用場运挫。

綜上: 要讓timer生效状共,必須保證該線程的runloop已啟動,而且其運行的runloop mode也要匹配谁帕。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末峡继,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子匈挖,更是在濱河造成了極大的恐慌碾牌,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件儡循,死亡現(xiàn)場離奇詭異舶吗,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)择膝,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進(jìn)店門誓琼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人调榄,你說我怎么就攤上這事踊赠。” “怎么了每庆?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵筐带,是天一觀的道長。 經(jīng)常有香客問我缤灵,道長伦籍,這世上最難降的妖魔是什么蓝晒? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮帖鸦,結(jié)果婚禮上芝薇,老公的妹妹穿的比我還像新娘。我一直安慰自己作儿,他們只是感情好洛二,可當(dāng)我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著攻锰,像睡著了一般晾嘶。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上娶吞,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天垒迂,我揣著相機(jī)與錄音,去河邊找鬼妒蛇。 笑死机断,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的绣夺。 我是一名探鬼主播吏奸,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼陶耍!你這毒婦竟也來了苦丁?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤物臂,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后产上,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體棵磷,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年晋涣,在試婚紗的時候發(fā)現(xiàn)自己被綠了仪媒。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡谢鹊,死狀恐怖算吩,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情佃扼,我是刑警寧澤偎巢,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站兼耀,受9級特大地震影響压昼,放射性物質(zhì)發(fā)生泄漏求冷。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一窍霞、第九天 我趴在偏房一處隱蔽的房頂上張望匠题。 院中可真熱鬧,春花似錦但金、人聲如沸韭山。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽钱磅。三九已至,卻和暖如春秃诵,著一層夾襖步出監(jiān)牢的瞬間续搀,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工菠净, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留禁舷,地道東北人。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓毅往,卻偏偏與公主長得像牵咙,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子攀唯,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容