NSTimer
計時器是一種很方便的對象。Foundation 框架中有個類叫做NSTimer,開發(fā)者可以指定絕對的日期和時間,以便到時執(zhí)行任務,也可以指定執(zhí)行任務的相對延遲時間。計時器還可以重復運行任務,有個與之相關(guān)聯(lián)的”間隔值”(interval)可用來指定任務的觸發(fā)頻率。比方說,可以每5秒輪詢某個資源。
- NSTimer方法
計時器要和”運行循環(huán)”(run loop)相關(guān)聯(lián),運行循環(huán)到時候回觸發(fā)任務。只有把計時器放在運行循環(huán)里,它才能正常觸發(fā)任務。如下是NSTimer的兩個常用的創(chuàng)建方法:
//本質(zhì)上創(chuàng)建一個定時器,并且把定時器自動添加到RunLoop的默認模式下工作,并且開始執(zhí)行工作
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti
target:(id)aTarget
selector:(SEL)aSelector
userInfo:(id)userInfo
repeats:(BOOL)yesOrNo;
//創(chuàng)建一個定時器,只是初始化不會工作,需要手動添加到runLoop的運行循環(huán)當中,否則定時器無法工作
//調(diào)用 NSRunLoop 對象的 addTimer:forMode: 方法
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti
target:(id)aTarget
selector:(SEL)aSelector
userInfo:(id)userInfo
repeats:(BOOL)yesOrNo;
//注意:定時器被創(chuàng)建后自動添加到當前當前線程的runLoop中,并且以Default模式工作
//定時器在runLoop的默認模式下工作,當主線程處理見面滑動事件時候,定時器將無法工作
//所以需要將定時器添加到占用模式下
//[[NSRunLoop mainRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes]
NSTimer會保留其目標對象
_timer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self selector:@selector(runTimer)
userInfo:nil
repeats:YES];
target與selector參數(shù)表示計時器將在哪個對象上調(diào)用哪個方法。計時器會保留其目標對象,object-c定時器會自動retain當前的使用者,如果不注意調(diào)用invalidate,則很容易引起循環(huán)引用導致內(nèi)存泄露。執(zhí)行完相關(guān)任務之后,一次性的計時器也會失效。
開發(fā)者若將計時器設置成重復執(zhí)行模式,那么必須自己調(diào)用invalidate方法,才能令其停止。
//計時器會保留其目標對象,等到自身“失效”時再釋放此對象
- (void)startTimer{
self.timer = [NSTimer scheduledTimerWithTimeInterval:3
target:self
selector:@selector(f)
userInfo:nil
repeats:YES];
}
- (void)endTimer{
[self.timer invalidate];
[self.timer = nil];
}
- (void)dealloc{
[self.timer invalidate];
[self.timer = nil];
}
//self -> timer timer - > target:self 形成保留環(huán) 無法釋放
由于 NSTimer 會引用住 self,而 self 又持有 NSTimer 對象,所以形成循環(huán)引用,
dealloc 永遠不會被執(zhí)行,timer 也永遠不會被釋放,定時任務會一直執(zhí)行下去
打破保留環(huán)
要想打破保留環(huán),只能改變實例例變量或者定時器.
- 從改變實例變量入手
如果想要通過系統(tǒng)回收self實例的時候令計時器無效,從而打破保留環(huán)是行不通的.因為計時器尚且有效,導致self實例保留計數(shù)不會降為0.因此系統(tǒng)不會回收,不會調(diào)用dealloc方法。所以這個方法是行不通的。
- 從改變計時器實例對象入手
從計時器入手,這就要求外界對象在釋放最后一個指向本類實例的對象之前,必須調(diào)用stopTimer方法。如果隨著某套公開的API對外公開給其他的開發(fā)者,那么也需要保證其他的開發(fā)者也這么做。所以這種方式很麻煩。
為什么不直接使用 weakself
我的第一直覺是像解決 Block 的循環(huán)引用一樣,所以嘗試 weakself 方案
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:3
target:weakSelf
selector:@selector(f)
userInfo:nil
repeats:YES];
實驗發(fā)現(xiàn)這種方案是無法解決循環(huán)引用的問題,這個問題其實很經(jīng)典,新手很容易混淆,以為用 weakSelf 就可以解決所有循環(huán)引用問題
- Block 使用weakSelf/self
回顧下,Block 中只是對變量 weakSelf 拷貝了一份,是拷貝變量而不是拷貝對象。即 Block 中也新定義了一個 weakSelf 對象,內(nèi)部實現(xiàn)代碼類似這樣__weak blockWeakSelf = weakSelf;,對象的 retainCount 沒有變化。如果拷貝的是 self,那么 Block 內(nèi)部實現(xiàn)代碼類似這樣__strong blockStrongSelf = self;,strong 類型的拷貝操作是會使對象的 retainCount 加1的
- NSTimer 可否使用weakSelf?
回到 NSTimer
The timer maintains a strong reference to this object until it (the timer) is invalidated
意思是要強應用這個變量的 也就是說,大概是這樣的, <code>__strong strongSelf = wself </code>強引用了一個弱應用的變量,結(jié)果還是強引用,也就是說strongSelf持有了wself所指向的對象(也即是self所只有的對象),這和你直接傳self進來是一樣的效果,。這也是為什么 block 里面用 strongSelf 強引用住 weakSelf,就可以讓 self 不釋放的原因
使用 Block 來解決循環(huán)引用
這個問題可以通過"塊"來解決。當然就是想要timer不要對控制器的方法強持有。解決的方法是在NSTimer 的基礎上,建一個分類(Category) 并實現(xiàn)一個類方法:
//.h
@interface NSTimer (NCYTimer)
+ (NSTimer *)ncy_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
block:(void(^)())block
repeats:(BOOL)repeats;
@end
//.m
@implementation NSTimer (NCYTimer)
+ (NSTimer *)ncy_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
block:(void(^)())block
repeats:(BOOL)repeats{
return [self scheduledTimerWithTimeInterval:interval
target:self
selector:@selector(ncy_blockHandle:)
userInfo:[block copy]//記得使用 copy
repeats:repeats];
}
+ (void)ncy_blockHandle:(NSTimer *)timer{
void (^block)() = timer.userInfo;
if (block){
block()
}
}
@end
調(diào)用過程注意循環(huán)引用
__weak typeof(self) weakSelf = self; //避免 block 強引用 self
self.timer = [NSTimer ncy_scheduledTimerWithTimeInterval:3
block:^{
typeof(weakSelf) strongSelf = weakSelf;
[strongSelf doSomething]; }
repeats:YES];
定義一個NSTimer的類別,在類別中定義一個類方法。類方法有一個類型為塊的參數(shù)(定義的塊位于棧上,為了防止塊被釋放,需要調(diào)用copy方法,將塊移到堆上)
這套方案將計時器應執(zhí)行的任務封裝成 block,然后再放到 userInfo 傳給計時器,block 作為參數(shù)傳遞時要 copy 到堆上,否則等到真正執(zhí)行的時候很可能會被釋放
這套方法依然存在循環(huán)引用的問題,但因為現(xiàn)在 NSTimer 引用的 target 是類對象,類對象本身是個單例,無需回收,而不是調(diào)用者,所以循環(huán)引用了也沒關(guān)系.基本的思想就是NSTimer會retain一個對象,現(xiàn)在讓它retain類對象。
調(diào)用的時候記得 block 里面要用 weakSelf,在外部先定義了一個弱引用,令其指向self,然后使塊捕獲這個引用,而不直接去捕獲普通的self變量。也就是說,self不會為計時器所保留。當塊開始執(zhí)行時,立刻生成strong引用,以保證實例在執(zhí)行期間持續(xù)存活。
疑問點:為什么類方法可以使用 self?
1.類方法可以調(diào)用類方法
2.類方法不可以調(diào)用實例方法,但是類方法可以通過創(chuàng)建對象來訪問實例方法
3.類方法不可以使用實例變量,類方法可以使用self,因為self不是
1.實例變量實例方法里面的self,是對象的首地址
2.類方法里面的self,是Class疑問點:全部定時器執(zhí)行的代碼放到一個單例去做,不會沖突嗎?定時器每執(zhí)行一個任務就是新建一個線程嗎
定時器每執(zhí)行一個任務并沒有新建一個線程,都是在當前線程,所以沖突是有可能的,假如某個任務很耗時,是會影響其他任務的執(zhí)行的,更多線程問題可以參考NSTimer和實現(xiàn)弱引用的timer的方式
ios10之后 新方法可以直接解決保留環(huán)問題
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
repeats:(BOOL)repeats
block:(void (^)(NSTimer *timer))block;
1.不再需要target,而傳入一個block,在block里面進行循環(huán)調(diào)用方法
2.如果在block里面[self runTimer],只需要外層弱引用(__weak)就可以避免保留環(huán)
3.block里面有個參數(shù)timer,就是方法返回的timer,特定情況下,可以在block里面設置timer invalidate來讓timer失效
要點
1.NSTimer 會保留其目標,直到計時器本身失效為止,調(diào)用 invalidate 方法可令計時器失效,另外,一次性的計時器在觸發(fā)完任務之后也會失效。
2.反復執(zhí)行任務的計時器,很容易引入保留環(huán),如果這種計時器的目標對象又保留了計時器本身,那肯定會導致保留環(huán)。這種環(huán)狀保留關(guān)系,可能是直接發(fā)生的,也可能是通過對象圖里的其他對象間接發(fā)生的。
3.可以擴充 NSTimer 的功能,用塊來打破保留環(huán)。不過除非NSTimer將來在公共接口里提供此功能,否則必須創(chuàng)建分類,將相關(guān)的代碼加入其中。