引言
我們?cè)谑褂胻imer的時(shí)候多多少少都遇到過一些坑,今天就來說說timer使用中的那些坑
1.循環(huán)引用導(dǎo)致的內(nèi)存泄露的問題
@implementation SecondController {
NSTimer *_testTimer;
}
- (void)viewDidLoad {
_testTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(handleTestTimer) userInfo:nil repeats:NO];
}
- (void)dealloc {
NSLog(@"dealloc!");
[_testTimer invalidate];
}
- (void)handleTestTimer {
NSLog(@"hello world!");
}
@end
我們可能寫過類似上面的代碼,一般情況下它是可以正常執(zhí)行的侵状,我們并沒有過多的去想timer的問題,但是實(shí)際上這樣寫是有問題的蝙砌。如果創(chuàng)建timer時(shí)repeats:YES随夸,再運(yùn)行的話,我們發(fā)現(xiàn)dealloc函數(shù)就永遠(yuǎn)不會(huì)調(diào)用了(我們這里SecondController是被另一個(gè)vc push進(jìn)來的)。
引起這個(gè)問題的原因就是:timer會(huì)強(qiáng)引用自己的target比默,在上面的例子中幻捏,我們的vc是強(qiáng)引用_testTimer對(duì)象的,但是創(chuàng)建這個(gè)timer的時(shí)候我們的target傳入的是self命咐,此時(shí)就導(dǎo)致了tiemr也強(qiáng)引用了self篡九,導(dǎo)致循環(huán)引用的產(chǎn)生。
準(zhǔn)確的說醋奠,timer在isValid為YES的時(shí)候是強(qiáng)引用自己的target的榛臼,所以一般我們都把invalidate的時(shí)機(jī)放在viewWillDisappear:或viewDidDisappear:的時(shí)候,這樣vc就會(huì)正常釋放了钝域。
@implementation SecondController {
NSTimer *_testTimer;
}
- (void)viewDidLoad {
[super viewDidLoad];
_testTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(handleTestTimer) userInfo:nil repeats:YES];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
if (_testTimer.isValid) {
[_testTimer invalidate];
}
}
- (void)dealloc {
NSLog(@"dealloc!");
}
- (void)handleTestTimer {
NSLog(@"hello world!");
}
@end
上面這樣就可以正常釋放了讽坏,但這里還要說一點(diǎn):
循環(huán)引用和內(nèi)存泄露還是稍有不同的
就拿我們的timer舉例,如果我們?cè)趧?chuàng)建timer的時(shí)候repeats:NO例证,但是觸發(fā)的時(shí)機(jī)是30s路呜,但是我們?cè)谶@個(gè)vc中停留小于30s,就會(huì)造成暫時(shí)的循環(huán)引用织咧,但是這種情況也不能說是內(nèi)存泄露胀葱,從我們退出vc開始到timer觸發(fā)時(shí)的這一段時(shí)間內(nèi),vc和timer造成了循環(huán)引用笙蒙,但是當(dāng)timer觸發(fā)后抵屿,你會(huì)發(fā)現(xiàn),vc的dealloc也被調(diào)用了捅位,此時(shí)vc和timer的內(nèi)存都能夠得到釋放轧葛,循環(huán)引用是有暫時(shí)性的,所以要理解循環(huán)引用和內(nèi)存泄露是稍有不同的艇搀。
可能有人會(huì)想尿扯,為什么非要用一個(gè)全部變量呢?直接:
[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(handleTestTimer) userInfo:nil repeats:YES];
不引入任何全局或局部變量不行嗎焰雕?答案是不行衷笋,即使像上面那樣,創(chuàng)建了timer矩屁,雖然沒有引入任何變量,但是依然會(huì)產(chǎn)生循環(huán)引用吝秕,如果repeats:YES泊脐,就會(huì)造成內(nèi)存泄露了。
為什么沒有任何全局或局部變量的timer仍然會(huì)循環(huán)引用烁峭?下面要說的坑會(huì)解決這個(gè)問題晨抡。
2.不用scheduledTimerWithTimeInterval方式創(chuàng)建timer的問題
@implementation SecondController {
NSTimer *_testTimer;
}
- (void)viewDidLoad {
[super viewDidLoad];
_testTimer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(handleTestTimer) userInfo:nil repeats:YES];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
if (_testTimer.isValid) {
[_testTimer invalidate];
}
}
- (void)dealloc {
NSLog(@"dealloc!");
[_testTimer invalidate];
}
- (void)handleTestTimer {
NSLog(@"hello world!");
}
@end
當(dāng)我們不用scheduledTimerWithTimeInterval方式創(chuàng)建timer的時(shí)候你會(huì)發(fā)現(xiàn)timer并不能正常的進(jìn)行回調(diào)。
有人會(huì)想,是不是這種情況要手動(dòng)觸發(fā)呢耘柱,但是即使我們調(diào)用:
[_testTimer fire];
也還是不能讓timer正常的回調(diào)如捅。
正確的做法是:
[[NSRunLoop currentRunLoop] addTimer:_testTimer forMode:NSDefaultRunLoopMode];
這里就解釋了為什么不設(shè)置成任何全局或局部變量的timer也會(huì)導(dǎo)致循環(huán)應(yīng)用的問題,因?yàn)槲覀円裻imer放到runloop里调煎,這個(gè)過程中timer就會(huì)一直在內(nèi)存中镜遣,即使你沒有給他賦值給任何變量,這也說明runloop在addTimer的時(shí)候也是強(qiáng)引用的士袄。
這里有一個(gè)關(guān)于timer很重要的一點(diǎn):NSTimer是基于Runloop的
這里重點(diǎn)不是要講解Runloop悲关,畢竟Runloop要比timer復(fù)雜的多,但為了幫助大家理解娄柳,這里簡(jiǎn)單的說說Runloop:
1.Runloop是用于管理線程的寓辱,它可以讓一個(gè)線程在有任務(wù)執(zhí)行的時(shí)候去執(zhí)行,沒有任務(wù)執(zhí)行的時(shí)候去休息赤拒。
2.一個(gè)線程總是對(duì)應(yīng)一個(gè)或零個(gè)Runloop對(duì)象秫筏,主線程默認(rèn)有一個(gè)Runloop對(duì)象,其他線程默認(rèn)沒有Runloop對(duì)象挎挖,但是可以通過currentRunLoop創(chuàng)建一個(gè)Runloop對(duì)象这敬。
3.Runloop可以保證程序循環(huán)執(zhí)行,而非線性執(zhí)行蕉朵,實(shí)際上Runloop就是用while循環(huán)來實(shí)現(xiàn)的崔涂,而且它并不是iOS系統(tǒng)特有的概念,只要能保證程序非線性的執(zhí)行的系統(tǒng)都有先關(guān)的概念始衅,像Android里的looper冷蚂。
3.timer觸發(fā)時(shí)機(jī)不準(zhǔn)的問題
timer發(fā)生回調(diào)的時(shí)機(jī)有時(shí)候并不一定完全由你設(shè)置的回調(diào)時(shí)間來決定,這要說到runloop mode汛闸,為了說明這個(gè)坑蝙茶,還是簡(jiǎn)單的介紹一下runloop mode。
1.一般常用的runloop mode是Default和Common modes蛉拙,除此之外尸闸,還有Connection彻亲、Modal孕锄、Event tracking等不同的mode。
2.一個(gè)線程可以對(duì)應(yīng)一個(gè)或零個(gè)runloop苞尝,一個(gè)runloop可以對(duì)應(yīng)一個(gè)或多個(gè)runloop mode畸肆,一個(gè)runloop mode中會(huì)有多個(gè)source。
3.一個(gè)runloop同時(shí)只能執(zhí)行一個(gè)runloop mode里的source宙址,系統(tǒng)對(duì)不同的runloop mode有不同的處理策略和優(yōu)先級(jí)轴脐。
我們常用的Default mode,優(yōu)先級(jí)很低,例如當(dāng)我們滾動(dòng)scrollview的時(shí)候大咱,此時(shí)runloop會(huì)切換到Event tracking的mode上恬涧,此時(shí)處于Default mode里的timer就不能得到執(zhí)行,直到這個(gè)runloop把mode切換到Default的時(shí)候timer才能被觸發(fā)碴巾,而沒有觸發(fā)timer的這段時(shí)間會(huì)按照timer設(shè)置的回調(diào)時(shí)間取整的進(jìn)行一次性觸發(fā)多次timer回調(diào)溯捆。為了保證timer的回調(diào)時(shí)機(jī)準(zhǔn)確,我們可以把timer放在Common modes里厦瓢。
4.子線程中timer的坑
@implementation SecondController {
NSTimer *_testTimer;
}
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"1:%p", [NSRunLoop currentRunLoop]);
[self performSelectorInBackground:@selector(testTimer) withObject:nil];
}
- (void)testTimer {
NSLog(@"2:%p", [NSRunLoop currentRunLoop]);
_testTimer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(handleTestTimer) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:_testTimer forMode:NSDefaultRunLoopMode];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
if (_testTimer.isValid) {
[_testTimer invalidate];
}
}
- (void)dealloc {
NSLog(@"dealloc!");
[_testTimer invalidate];
}
- (void)handleTestTimer {
NSLog(@"hello world!");
}
@end
當(dāng)我們用performSelector的方式把timer放到子線程中的runloop里時(shí)發(fā)現(xiàn)timer又不好使了提揍。
這個(gè)坑的主要原因是,子線程默認(rèn)是沒有runloop的煮仇,currentRunloop的方式是可以獲得runloop的劳跃,但是此時(shí)的這個(gè)runloop并沒有run,我們要在[[NSRunLoop currentRunLoop] addTimer:_testTimer forMode:NSDefaultRunLoopMode];后調(diào)用[[NSRunLoop currentRunLoop] run];才可以使得這個(gè)timer正常工作的浙垫。
結(jié)論
一個(gè)小小的timer當(dāng)我們仔細(xì)去研究的時(shí)候發(fā)現(xiàn)它大有文章可做刨仑,好多東西都值得我們深入研究,例如timer會(huì)強(qiáng)引用自己的target導(dǎo)致循環(huán)引用绞呈,所以我們?cè)诿看斡胻imer的時(shí)候都要小心贸人,當(dāng)timer頁面要消失的時(shí)候我們總是要調(diào)用invalidate, 這樣很麻煩,我們能不能使用timer的時(shí)候不用去考慮它的釋放問題佃声,這個(gè)問題就是自釋放的問題艺智,我們可以想想能不能實(shí)現(xiàn)一個(gè)自釋放的timer。