一树姨、什么是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é)果如下:
觀察輸出,我們會發(fā)現(xiàn)53分58秒的時候我們就對測試對象執(zhí)行了release操作讶凉,但是知道54分03秒的時候timer觸發(fā)完方法以后染乌,該對象才實際的執(zhí)行了dealloc方法。這就證明一次性的timer也會retain它的方法接收者懂讯,直到自己失效為之荷憋。
測試重復(fù)性的timer的結(jié)果如下:
觀察輸出我們發(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é)果如下:
仔細(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é)果如下:
觀察發(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é)果如下:
其中妆够,第一行注釋的代碼,可以打印輸出該線程的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也要匹配谁帕。