????計時器是一種很方便也很用的對象。Foundation框架中有個類叫做NSTimer,開發(fā)者可以指定絕對的日期與時間,以便到時執(zhí)行任務(wù),也可以指定執(zhí)行任務(wù)的相對延遲時間。計時器可以重復(fù)運(yùn)行任務(wù),有個與之相關(guān)的"間隔值"(interval)可用來指定任務(wù)的觸發(fā)頻率。比方說,可以每5秒輪詢某個資源。
????計時器要和"運(yùn)行循環(huán)"(run loop)相關(guān)聯(lián),運(yùn)行循環(huán)到時候會觸發(fā)任務(wù)。創(chuàng)建NSTimer時,可以將其"預(yù)先安排"在當(dāng)前的運(yùn)行循環(huán)中,也可以先創(chuàng)建好,然后由開發(fā)者自己來調(diào)度。無論采用哪種方式,只有把計時器放在運(yùn)行循環(huán)里,它才能正常觸發(fā)任務(wù)。例如,下面這個方法可以創(chuàng)建計時器,并將其預(yù)先安排在當(dāng)前運(yùn)行循環(huán)中:
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)seconds target:(id)target selector:(SEL)selector userInfo:(id)userInfo repeats:(BOOL)repeat
????用此方法創(chuàng)建出來的計時器,會在指定的間隔時間之后執(zhí)行任務(wù)。也可以令其反復(fù)執(zhí)行任務(wù),直到開發(fā)者稍后將其手動關(guān)閉為止。target與selector參數(shù)表示計時器將在哪個對象上調(diào)用哪個方法。計時器會保留其目標(biāo)對象,等到自身“失效”時再釋放此對象。調(diào)用invalidate方法可令計時器失效;執(zhí)行完相關(guān)任務(wù)之后,一次性的計時器也會失效。開發(fā)者若將計時器設(shè)置成重復(fù)執(zhí)行模式,那么必須自己調(diào)用invalidate方法,才能令其停止。
????由于計時器會保留其目標(biāo)對象,所以反復(fù)執(zhí)行任務(wù)通常會導(dǎo)致應(yīng)用程序出問題。也就是說,設(shè)置成重復(fù)執(zhí)行模式的那種計時器,很容易引入"保留環(huán)"。要想知道其中緣由,請看下列代碼:
#import <Foundation/Foundation.h>
@interface EOCClass : NSObject
- (void)startPolling;
- (void)stopPolling;
@end
@implementation EOCClass {
NSTimer *_pollTimer;
}
- (id)init {
return [super init];
}
- (void)dealloc {
[_pollTimer invalidate];
}
- (void)stopPolling{
[_pollTimer invalidate];
_pollTimer = nil;
}
- (void)startPolling{
_pollTimer = [NSTimer scheduledTimeWithTimeInterval:5.0 target:self selector:@selector(p_doPoll) userInfo:nil repeats:YES];
}
- (void)p_doPoll{
//Poll the resource
}
@end
????能看出問題嗎?如果創(chuàng)建了本類的實例,并調(diào)用其startPolling方法,那會如何呢?創(chuàng)建計時器的時候,由于目標(biāo)對象是self,所以要保留此實例。然而,因為計時器是用實例變量存放的,所以實例也保留了計時器。(回想一下,第30條說過,在ARC環(huán)境中,這種情況將執(zhí)行保留操作。)于是,就成生了"保留環(huán)",如果此環(huán)能在某一時刻打破,那就不會出什么問題。然而要想打破保留環(huán),只能改變實例變量或令計時器無效。所以說,要么調(diào)用stopPolling,要么令系統(tǒng)將此實例回收,只有這樣才能打破保留環(huán)。除非使用該類的所有代碼均在你的掌控之中,否則無法確保stopPolling一定會調(diào)用。而且即便能滿足此條件,這種通過調(diào)用某方法來避免內(nèi)存泄漏的做法,也不是一個好主意。另外,如果想在系統(tǒng)回收本類實例的過程中令計時器無效,從而打破保留環(huán),那又會陷入死結(jié)。因為在計時器對象尚且有效時,EOCClass實例的保留計數(shù)絕不會降為0,因此系統(tǒng)也絕不會將其回收。而現(xiàn)在又沒人來調(diào)用invalidate方法,所以計時器將一直處于有效狀態(tài)。圖7-1演示了此情況。

????當(dāng)指向EOCClass實例的最后一個外部引用移走之后,該實例仍然會繼續(xù)存活,因為計時器還保留著它。而計時器對象也不可能為系統(tǒng)所釋放,因為實例中還有個強(qiáng)引用正在指向它。更糟糕的是:除了計時器之外,已經(jīng)沒有別的引用再指向這個實例了,于是該實例就永遠(yuǎn)"丟失"了。而除了該實例之外,又沒有其他引用指向計時器。于是,內(nèi)存就泄漏了。這種內(nèi)存泄漏問題尤為嚴(yán)重,因為計時器還將繼續(xù)反復(fù)地執(zhí)行輪詢?nèi)蝿?wù)。要是每次輪詢時都得聯(lián)網(wǎng)下載數(shù)據(jù)的話,那么程序就會一直下載數(shù)據(jù),這又更容易導(dǎo)致其他內(nèi)存泄漏的問題。
????單從計時器本身入手,很難解決這個問題??梢砸笸饨鐚ο笤卺尫抛詈笠粋€指向本實例的引用之前,必須先調(diào)用stopPolling方法。然而這種情況無法通過代碼檢測出來,此外,假如該類隨著某套公開的API對外發(fā)布給其他開發(fā)者,那么無法保證他們一定會調(diào)用此方法。
????這個問題可通過"塊"來解決。雖然計時器當(dāng)前并不直接支持塊,但是可以用下面這段代碼為其添加此功能:
#import <Foundation/Foundation.h>
+ (NSTimer *)eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void(^)())block repeats:(BOOL)repeats;
@end
@implementation NSTimer (EOCBlocksSupport)
+ (NSTimer *)eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void(^)())block repeats:(BOOL)repeats
{
return [self scheduleTimerWithTimeInterval:interval target:self selector:@selector:(eoc_blockInvoke:) userInfo:[block copy] repeats:repeats];
}
+ (void)eoc_blockInvoke:(NSTimer *)timer{
void (^block)() = timer.userInfo;
if(block) {
block();
}
}
@end