引言
我們在使用timer的時候多多少少都遇到過一些坑,今天就來說說timer使用中的那些坑
1.循環引用導致的內存泄露的問題
@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
我們可能寫過類似上面的代碼,一般情況下它是可以正常執行的,我們并沒有過多的去想timer的問題,但是實際上這樣寫是有問題的。如果創建timer時repeats:YES,再運行的話,我們發現dealloc函數就永遠不會調用了(我們這里SecondController是被另一個vc push進來的)。
引起這個問題的原因就是:timer會強引用自己的target,在上面的例子中,我們的vc是強引用_testTimer對象的,但是創建這個timer的時候我們的target傳入的是self,此時就導致了tiemr也強引用了self,導致循環引用的產生。
準確的說,timer在isValid為YES的時候是強引用自己的target的,所以一般我們都把invalidate的時機放在viewWillDisappear:或viewDidDisappear:的時候,這樣vc就會正常釋放了。
@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
上面這樣就可以正常釋放了,但這里還要說一點:
循環引用和內存泄露還是稍有不同的
就拿我們的timer舉例,如果我們在創建timer的時候repeats:NO,但是觸發的時機是30s,但是我們在這個vc中停留小于30s,就會造成暫時的循環引用,但是這種情況也不能說是內存泄露,從我們退出vc開始到timer觸發時的這一段時間內,vc和timer造成了循環引用,但是當timer觸發后,你會發現,vc的dealloc也被調用了,此時vc和timer的內存都能夠得到釋放,循環引用是有暫時性的,所以要理解循環引用和內存泄露是稍有不同的。
可能有人會想,為什么非要用一個全部變量呢?直接:
[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(handleTestTimer) userInfo:nil repeats:YES];
不引入任何全局或局部變量不行嗎?答案是不行,即使像上面那樣,創建了timer,雖然沒有引入任何變量,但是依然會產生循環引用,如果repeats:YES,就會造成內存泄露了。
為什么沒有任何全局或局部變量的timer仍然會循環引用?下面要說的坑會解決這個問題。
2.不用scheduledTimerWithTimeInterval方式創建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
當我們不用scheduledTimerWithTimeInterval方式創建timer的時候你會發現timer并不能正常的進行回調。
有人會想,是不是這種情況要手動觸發呢,但是即使我們調用:
[_testTimer fire];
也還是不能讓timer正常的回調。
正確的做法是:
[[NSRunLoop currentRunLoop] addTimer:_testTimer forMode:NSDefaultRunLoopMode];
這里就解釋了為什么不設置成任何全局或局部變量的timer也會導致循環應用的問題,因為我們要把timer放到runloop里,這個過程中timer就會一直在內存中,即使你沒有給他賦值給任何變量,這也說明runloop在addTimer的時候也是強引用的。
這里有一個關于timer很重要的一點:NSTimer是基于Runloop的
這里重點不是要講解Runloop,畢竟Runloop要比timer復雜的多,但為了幫助大家理解,這里簡單的說說Runloop:
1.Runloop是用于管理線程的,它可以讓一個線程在有任務執行的時候去執行,沒有任務執行的時候去休息。
2.一個線程總是對應一個或零個Runloop對象,主線程默認有一個Runloop對象,其他線程默認沒有Runloop對象,但是可以通過currentRunLoop創建一個Runloop對象。
3.Runloop可以保證程序循環執行,而非線性執行,實際上Runloop就是用while循環來實現的,而且它并不是iOS系統特有的概念,只要能保證程序非線性的執行的系統都有先關的概念,像Android里的looper。
3.timer觸發時機不準的問題
timer發生回調的時機有時候并不一定完全由你設置的回調時間來決定,這要說到runloop mode,為了說明這個坑,還是簡單的介紹一下runloop mode。
1.一般常用的runloop mode是Default和Common modes,除此之外,還有Connection、Modal、Event tracking等不同的mode。
2.一個線程可以對應一個或零個runloop,一個runloop可以對應一個或多個runloop mode,一個runloop mode中會有多個source。
3.一個runloop同時只能執行一個runloop mode里的source,系統對不同的runloop mode有不同的處理策略和優先級。
我們常用的Default mode,優先級很低,例如當我們滾動scrollview的時候,此時runloop會切換到Event tracking的mode上,此時處于Default mode里的timer就不能得到執行,直到這個runloop把mode切換到Default的時候timer才能被觸發,而沒有觸發timer的這段時間會按照timer設置的回調時間取整的進行一次性觸發多次timer回調。為了保證timer的回調時機準確,我們可以把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
當我們用performSelector的方式把timer放到子線程中的runloop里時發現timer又不好使了。
這個坑的主要原因是,子線程默認是沒有runloop的,currentRunloop的方式是可以獲得runloop的,但是此時的這個runloop并沒有run,我們要在[[NSRunLoop currentRunLoop] addTimer:_testTimer forMode:NSDefaultRunLoopMode];后調用[[NSRunLoop currentRunLoop] run];才可以使得這個timer正常工作的。
結論
一個小小的timer當我們仔細去研究的時候發現它大有文章可做,好多東西都值得我們深入研究,例如timer會強引用自己的target導致循環引用,所以我們在每次用timer的時候都要小心,當timer頁面要消失的時候我們總是要調用invalidate, 這樣很麻煩,我們能不能使用timer的時候不用去考慮它的釋放問題,這個問題就是自釋放的問題,我們可以想想能不能實現一個自釋放的timer。