NSTimer和它調(diào)用的函數(shù)對(duì)象間到底發(fā)生什么
timer會(huì)在未來(lái)的某個(gè)時(shí)刻執(zhí)行一次或者多次我們指定的方法,這也就牽扯出一個(gè)問題姥宝,如何保證timer在未來(lái)的某個(gè)時(shí)刻觸發(fā)指定事件的時(shí)候殴瘦,我們指定的方法是有效的呢妓雾?
解決方法很簡(jiǎn)單,只要將指定給timer的方法的接收者retain一份就搞定了殖熟,實(shí)際上系統(tǒng)也是這樣做的彻犁。不管是重復(fù)性的timer還是一次性的timer都會(huì)對(duì)它的方法的接收者進(jìn)行retain,這兩種timer的區(qū)別在于“一次性的timer在完成調(diào)用以后會(huì)自動(dòng)將自己invalidate,而重復(fù)的timer則將永生叫胁,直到你調(diào)用invalidate方法終止」保”
下面看一個(gè)小例子
SvTestObject.h
`#import <Foundation/Foundation.h>
@interface SvTestObject : NSObject
/*
* @brief timer響應(yīng)函數(shù)驼鹅,只是用來(lái)做測(cè)試
*/
- (void)timerAction:(NSTimer*)timer;
@end
SvTestObject.m
`#import "SvTestObject.h"
@implementation SvTestObject
- (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
SvTimerAppDelegate.m
- (void)applicationDidBecomeActive:(UIApplication *)application
{
// test Timer retain target
[self testNonRepeatTimer];
// [self testRepeatTimer];
}
- (void)testNonRepeatTimer
{
NSLog(@"Test retatin target for non-repeat timer!");
SvTestObject *testObject = [[SvTestObject alloc] init];
[NSTimer scheduledTimerWithTimeInterval:5 target:testObject selector:@selector(timerAction:) userInfo:nil repeats:NO];
[testObject release];
NSLog(@"Invoke release to testObject!");
}
- (void)testRepeatTimer
{
NSLog(@"Test retain target for repeat Timer");
SvTestObject *testObject2 = [[SvTestObject alloc] init];
[NSTimer scheduledTimerWithTimeInterval:5 target:testObject2 selector:@selector(timerAction:) userInfo:nil repeats:YES];
[testObject2 release];
NSLog(@"Invoke release to testObject2!");
}
上面的簡(jiǎn)單例子中,我們自定義了一個(gè)繼承自NSObject的類SvTestObject急鳄,在這個(gè)類的init谤民,dealloc和它的timerAction三個(gè)方法中分別打印信息。然后在appDelegate中分別測(cè)試一個(gè)單次執(zhí)行的timer和一個(gè)重復(fù)執(zhí)行的timer對(duì)方法接受者是否做了retain操作疾宏,因此我們?cè)趦煞N情況下都是shedule完timer之后立馬對(duì)該測(cè)試對(duì)象執(zhí)行release操作张足。
測(cè)試單次執(zhí)行的timer的結(jié)果如下:
觀察輸出,我們會(huì)發(fā)現(xiàn)53分58秒的時(shí)候我們就對(duì)測(cè)試對(duì)象執(zhí)行了release操作坎藐,但是知道54分03秒的時(shí)候timer觸發(fā)完方法以后为牍,該對(duì)象才實(shí)際的執(zhí)行了dealloc方法。這就證明一次性的timer也會(huì)retain它的方法接收者岩馍,直到自己失效為之碉咆。
測(cè)試重復(fù)性的timer的結(jié)果如下:
觀察輸出我們發(fā)現(xiàn),這個(gè)重復(fù)性的timer一直都在周期性的調(diào)用我們?yōu)樗付ǖ姆椒ㄖ鳎覝y(cè)試的對(duì)象也一直沒有真正的被釋放疫铜。
通過以上小例子,我們可以發(fā)現(xiàn)在timer對(duì)它的接收者進(jìn)行retain双谆,從而保證了timer調(diào)用時(shí)的正確性壳咕,但是又引入了接收者的內(nèi)存管理問題。特別是對(duì)于重復(fù)性的timer顽馋,它所引用的對(duì)象將一直存在谓厘,將會(huì)造成內(nèi)存泄露。
有問題就有應(yīng)對(duì)方法寸谜,NSTimer提供了一個(gè)方法invalidate竟稳,讓我們可以解決這種問題。不管是一次性的還是重復(fù)性的timer熊痴,在執(zhí)行完invalidate以后都會(huì)變成無(wú)效他爸,因此對(duì)于重復(fù)性的timer我們一定要有對(duì)應(yīng)的invalidate。
突然想起一種自欺欺人的寫法果善,不知道你們有沒有這么寫過
·#import "SvCheatYourself.h"
@interface SvCheatYourself () {
NSTimer *_timer;
}
@end
@implementation SvCheatYourself
- (id)init
{
self = [super init];
if (self) {
_timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(testTimer:) userInfo:nil repeats:YES];
}
return self;
}
- (void)dealloc
{
// 自欺欺人的寫法诊笤,永遠(yuǎn)都不會(huì)執(zhí)行到,除非你在外部手動(dòng)invalidate這個(gè)timer
[_timer invalidate];
[super dealloc];
}
- (void)testTimer:(NSTimer*)timer
{
NSLog(@"haha!");
}
@end
總結(jié):
timer都會(huì)對(duì)它的target進(jìn)行retain岭埠,我們需要小心對(duì)待這個(gè)target的生命周期問題盏混,尤其是重復(fù)性的timer蔚鸥。
NSTimer為什么要添加到RunLoop中才會(huì)有作用
前面的例子中我們使用的是一種便利方法,它其實(shí)是做了兩件事:首先創(chuàng)建一個(gè)timer许赃,然后將該timer添加到當(dāng)前runloop的default mode中止喷。也就是這個(gè)便利方法給我們?cè)斐闪酥灰獎(jiǎng)?chuàng)建了timer就可以生效的錯(cuò)覺,我們當(dāng)然可以自己創(chuàng)建timer混聊,然后手動(dòng)的把它添加到指定runloop的指定mode中去弹谁。
NSTimer其實(shí)也是一種資源,如果看過多線程變成指引文檔的話句喜,我們會(huì)發(fā)現(xiàn)所有的source如果要起作用预愤,就得加到runloop中去。同理timer這種資源要想起作用咳胃,那肯定也需要加到runloop中才會(huì)又效嘍植康。如果一個(gè)runloop里面不包含任何資源的話,運(yùn)行該runloop時(shí)會(huì)立馬退出展懈。你可能會(huì)說(shuō)那我們APP的主線程的runloop我們沒有往其中添加任何資源销睁,為什么它還好好的運(yùn)行。我們不添加存崖,不代表框架沒有添加冻记,如果有興趣的話你可以打印一下main thread的runloop,你會(huì)發(fā)現(xiàn)有很多資源来惧。
下面我們看一個(gè)小例子:
- (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!");
}
這個(gè)小例子中我們新建了一個(gè)timer冗栗,為它指定了有效的target和selector,并指出了1秒后觸發(fā)該消息供搀,運(yùn)行結(jié)果如下:
觀察發(fā)現(xiàn)這個(gè)消息永遠(yuǎn)也不會(huì)觸發(fā)隅居,原因很簡(jiǎn)單,我們沒有將timer添加到runloop中趁曼。
總結(jié): 必須得把timer添加到runloop中军浆,它才會(huì)生效棕洋。
NSTimer加到了RunLoop中但遲遲的不觸發(fā)事件
為什么明明添加了挡闰,但是就是不按照預(yù)先的邏輯觸發(fā)事件呢?掰盘?摄悯?原因主要有以下兩個(gè):
1、runloop是否運(yùn)行
每一個(gè)線程都有它自己的runloop愧捕,程序的主線程會(huì)自動(dòng)的使runloop生效奢驯,但對(duì)于我們自己新建的線程,它的runloop是不會(huì)自己運(yùn)行起來(lái)次绘,當(dāng)我們需要使用它的runloop時(shí)瘪阁,就得自己?jiǎn)?dòng)撒遣。
那么如果我們把一個(gè)timer添加到了非主線的runloop中,它還會(huì)按照預(yù)期按時(shí)觸發(fā)嗎管跺?下面請(qǐng)看一段測(cè)試程序:
- (void)applicationDidBecomeActive:(UIApplication *)application
{
[NSThread detachNewThreadSelector:@selector(testTimerSheduleToRunloop1) toTarget:self withObject:nil];
}
// 測(cè)試把timer加到不運(yùn)行的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的內(nèi)容就可以看出义黎,timer卻是已經(jīng)被添加進(jìn)去
//NSLog(@"the thread's runloop: %@", [NSRunLoop currentRunLoop]);
// 打開下面一行, 該線程的runloop就會(huì)運(yùn)行起來(lái),timer才會(huì)起作用
//[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
[testObject4 release];
NSLog(@"invoke release to testObject4");
[pool release];
}
- (void)applicationWillResignActive:(UIApplication *)application
{
NSLog(@"SvTimerSample Will resign Avtive!");
}
上面的程序中豁跑,我們新創(chuàng)建了一個(gè)線程廉涕,然后創(chuàng)建一個(gè)timer,并把它添加當(dāng)該線程的runloop當(dāng)中艇拍,但是運(yùn)行結(jié)果如下:
觀察運(yùn)行結(jié)果狐蜕,我們發(fā)現(xiàn)這個(gè)timer知道執(zhí)行退出也沒有觸發(fā)我們指定的方法,如果我們把上面測(cè)試程序中“//[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];”這一行的注釋去掉卸夕,則timer將會(huì)正確的掉用我們指定的方法层释。
2、mode是否正確
我們前面自己動(dòng)手添加runloop的時(shí)候快集,可以看到有一個(gè)參數(shù)runloopMode湃累,這個(gè)參數(shù)是干嘛的呢?
前面提到了要想timer生效碍讨,我們就得把它添加到指定runloop的指定mode中去治力,通常是主線程的defalut mode。但有時(shí)我們這樣做了勃黍,卻仍然發(fā)現(xiàn)timer還是沒有觸發(fā)事件宵统。這是為什么呢?
這是因?yàn)閠imer添加的時(shí)候覆获,我們需要指定一個(gè)mode马澈,因?yàn)橥痪€程的runloop在運(yùn)行的時(shí)候,任意時(shí)刻只能處于一種mode弄息。所以只能當(dāng)程序處于這種mode的時(shí)候痊班,timer才能得到觸發(fā)事件的機(jī)會(huì)。
舉個(gè)不恰當(dāng)?shù)睦幽×浚覀冋f(shuō)兄弟幾個(gè)分別代表runloop的mode涤伐,timer代表他們自己的才水桶,然后一群人去排隊(duì)打水缨称,只有一個(gè)水龍頭凝果,那么同一時(shí)刻,肯定只能有一個(gè)人處于接水的狀態(tài)睦尽。也就是說(shuō)你雖然給了老二一個(gè)桶器净,但是還沒輪到它,那么你就得等当凡,只有輪到他的時(shí)候你的水桶才能碰上用場(chǎng)山害。
綜上: 要讓timer生效纠俭,必須保證該線程的runloop已啟動(dòng),而且其運(yùn)行的runloopmode也要匹配浪慌。