面試中,經(jīng)常會(huì)問道 NSTimer 循環(huán)引用的問題。
閑話少敘。下面來講講 NSTimer 為什么會(huì)造成循環(huán)引用?
使用 NSTimer 的 block 的方式來創(chuàng)建定時(shí)器。
一般情況下,我們會(huì)把 NSTimer 定義成當(dāng)前控制器的一個(gè)屬性/成員變量。
@implementation ViewController {
NSTimer *_timer;
}
到此步為止,造成了一個(gè)單向引用
ViewController -> NSTimer
使用 NSTimer 的 block 語(yǔ)法,來創(chuàng)建定時(shí)器任務(wù)。
- (void)blockTimer {
// 創(chuàng)建一個(gè) _timer。
_timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
// 此控制器的內(nèi)部并沒有捕獲控制器,于是就沒有造成對(duì)控制器的強(qiáng)引用。
NSLog(@"%@",@"timer 開始執(zhí)行。");
}];
[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSDefaultRunLoopMode];
由于定時(shí)器執(zhí)行任務(wù)的 block,并沒有引用 self,到次為止,引用關(guān)系仍然是單向的。
重寫 dealloc 方法來檢測(cè),當(dāng)前控制器是否可以被銷毀。
- (void)dealloc {
NSLog(@"%@",@"控制器不能被銷毀?");
}
運(yùn)行結(jié)果:
2017-11-06 19:14:50.195 CodeForNSTimerRetainCycle[18912:19230566] timer 開始執(zhí)行。
2017-11-06 19:14:51.196 CodeForNSTimerRetainCycle[18912:19230566] timer 開始執(zhí)行。
2017-11-06 19:14:52.196 CodeForNSTimerRetainCycle[18912:19230566] timer 開始執(zhí)行。
2017-11-06 19:14:53.196 CodeForNSTimerRetainCycle[18912:19230566] timer 開始執(zhí)行。
timer 可以正常執(zhí)行。
當(dāng)點(diǎn)擊了返回按鈕,退出此控制器的 Log 輸出。
2017-11-06 19:15:36.344 CodeForNSTimerRetainCycle[18949:19234509] 控制器不能被銷毀?
2017-11-06 19:15:36.482 CodeForNSTimerRetainCycle[18949:19234509] timer 開始執(zhí)行。
2017-11-06 19:15:37.483 CodeForNSTimerRetainCycle[18949:19234509] timer 開始執(zhí)行。
2017-11-06 19:15:38.482 CodeForNSTimerRetainCycle[18949:19234509] timer 開始執(zhí)行。
2017-11-06 19:15:39.482 CodeForNSTimerRetainCycle[18949:19234509] timer 開始執(zhí)行。
控制器可以正常銷毀。但是 NSTimer 仍然在繼續(xù)執(zhí)行。

當(dāng)點(diǎn)擊控制器的返回按鈕時(shí),圖中那條白色的箭頭斷開,當(dāng)前控制器又沒有其他的對(duì)象引用(除了 navigationController,但pop 的時(shí)候,這條連接已經(jīng)斷開了)。所以,控制器可以被正確釋放。
2017-11-06 19:15:36.344 CodeForNSTimerRetainCycle[18949:19234509] 控制器不能被銷毀?
NSTimer 沒有被釋放,是因?yàn)?,?dāng)我們把 NSTimer 添加到運(yùn)行循環(huán)時(shí),運(yùn)行循環(huán)強(qiáng)引用了這個(gè) NSTimer(也就是圖中紅色的箭頭)。
而 Runloop 是無法銷毀的(UI線程)。
所以,這條紅色的箭頭,就無法斷開。NSTimer 不能被釋放。
于是就出現(xiàn)了控制器被正確銷毀,但是在控制器里創(chuàng)建的 NSTimer 卻可以仍然運(yùn)行的情況。
是 Runloop 強(qiáng)引用了這個(gè) NSTimer。
但這種寫法,并不阻礙當(dāng)前控制器的釋放。
但為了保證,NSTimer 在控制器釋放的時(shí)候,就不要在繼續(xù)執(zhí)行了,可以在 dealloc 方法里,讓 NSTimer 過期。
- (void)dealloc {
NSLog(@"%@",@"控制器不能被銷毀?");
[_timer invalidate];
}
使用 NSTimer 的 block 的方式來創(chuàng)建定時(shí)器,在 block 內(nèi)部訪問 self。
在 block 的內(nèi)部訪問 self,肯定會(huì)造成循環(huán)引用,道理也很簡(jiǎn)單。

解決辦法,使用經(jīng)典的 weak-strong dance即可。
- (void)blockTimer {
// 創(chuàng)建一個(gè) _timer。
__weak typeof(self) weakSelf = self;
_timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
__strong typeof(weakSelf) strongSelf = weakSelf;
// 此控制器的內(nèi)部并沒有捕獲控制器,于是就沒有造成對(duì)控制器的強(qiáng)引用。
// NSLog(@"%@",@"timer 開始執(zhí)行。");
strongSelf.view.backgroundColor = [UIColor colorWithRed:arc4random_uniform(256)/255.0 green:arc4random_uniform(256)/255.0 blue:arc4random_uniform(256)/255.0 alpha:arc4random_uniform(256)/255.0];
NSLog(@"%@",strongSelf.view);
}];
[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSDefaultRunLoopMode];
運(yùn)行結(jié)果:
2017-11-06 19:33:36.518 CodeForNSTimerRetainCycle[19195:19304854] 控制器不能被銷毀?
控制器可以正常退出,NSTimer 也不會(huì)繼續(xù)執(zhí)行了。
使用 NSTimer 的 target 方式來來創(chuàng)建定時(shí)任務(wù)。
使用NSTimer 的 target 方式來來創(chuàng)建定時(shí)任務(wù)。NSTimer 一定會(huì)強(qiáng)引用住當(dāng)前控制器。
#pragma mark timer 的 target 方式
- (void)targetTimer {
// 這種方式創(chuàng)建,timer 會(huì)強(qiáng)引用 self。導(dǎo)致這個(gè)控制器無法被銷毀。
_timer = [[NSTimer alloc] initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:1] interval:1 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
}
運(yùn)行結(jié)果:
2017-11-06 19:35:56.565 CodeForNSTimerRetainCycle[19241:19313152] timer 事件執(zhí)行!
2017-11-06 19:35:57.565 CodeForNSTimerRetainCycle[19241:19313152] timer 事件執(zhí)行!
2017-11-06 19:35:58.565 CodeForNSTimerRetainCycle[19241:19313152] timer 事件執(zhí)行!
當(dāng)pop 當(dāng)前控制器的時(shí)候,dealloc 方法也沒有被執(zhí)行。說明當(dāng)前控制器沒有被釋放。
為什么使用了 target 的方式來NSTimer 定時(shí)任務(wù)的時(shí)候,當(dāng)前控制器不能被釋放呢?
因?yàn)?NSTimer 強(qiáng)引用了這個(gè)控制器。
為什么 NSTimer 通過 target 的方式創(chuàng)建定時(shí)任務(wù)的時(shí)候,要強(qiáng)引用這個(gè)控制器呢?
NSTimer 的任務(wù)來源的 SEL 來自于這個(gè)控制器,只有在當(dāng)前控制器上才能通過 SEL 找到方法的實(shí)現(xiàn)。如果 控制器不被強(qiáng)引用住,那么會(huì)會(huì)影響 NSTimer 定時(shí)任務(wù)的執(zhí)行。
所以,為了保證 NSTimer 任務(wù)能夠定時(shí)的執(zhí)行,就必須強(qiáng)引用這個(gè)控制器。
也就是說,NSTimer 內(nèi)部有一個(gè) __strong target,強(qiáng)引用了這個(gè)控制器,于是就造成了循環(huán)引用。
兩個(gè)對(duì)象,如果雙發(fā)都被強(qiáng)引用了,一般 strong,一般 weak 即可。
但是 NSTimer 是系統(tǒng)的類,且當(dāng)前控制器的傳遞是使用 target 的方式。又不是我們自己定義類,把屬性改成 weak 修飾就好了。
幸好的是,NSTimer 提供了一個(gè)可以斷開和當(dāng)前 target 強(qiáng)引用關(guān)系的方法。
[_timer invalidate];
當(dāng)我們調(diào)用這個(gè)方法的時(shí)候。
- NSTimer 會(huì)斷開自己和 target 的強(qiáng)引用關(guān)系。
- Runloop 會(huì)斷開自己和 NSTimer 的強(qiáng)引用關(guān)系。
第一條解決了控制器釋放的問題。
第二條解決了在控制器釋放之后,NSTimer仍然在繼續(xù)執(zhí)行的問題。
于是就開心的在當(dāng)前控制器的 dealloc 方法里加了這么一行代碼。
- (void)dealloc {
NSLog(@"%@",@"控制器不能被銷毀?");
[_timer invalidate];
}
運(yùn)行結(jié)果:
2017-11-06 19:43:58.682 CodeForNSTimerRetainCycle[19305:19325055] timer 事件執(zhí)行!
2017-11-06 19:43:59.682 CodeForNSTimerRetainCycle[19305:19325055] timer 事件執(zhí)行!
2017-11-06 19:44:00.682 CodeForNSTimerRetainCycle[19305:19325055] timer 事件執(zhí)行!
2017-11-06 19:44:01.682 CodeForNSTimerRetainCycle[19305:19325055] timer 事件執(zhí)行!
發(fā)現(xiàn)等控制器pop 出的時(shí)候,NSTimer 既沒斷開和控制器的強(qiáng)引用,也沒有被 Runloop 斷開對(duì)自身的強(qiáng)引用。
問題出在哪?
- NSTimer & ViewController 雙向引用了。
- Runloop & NSTimer 單向引用了。
- NSTimer invalidate 方法寫在了 ViewController 的 dealloc 方法里了。
答案呼之欲出了:
由于第一條,NSTimer & ViewController 雙向引用了,當(dāng)前控制器根本無法被釋放。
從而無法執(zhí)行到 dealloc , 就無法執(zhí)行 [_timer invalidate] 這個(gè)方法。
但這個(gè)方法,又是 NSTimer 斷開和控制器以及 runloop 的核心方法。
所以,就造成了無法控制器無法釋放,以及 NSTimer 仍然在繼續(xù)執(zhí)行的問題。
終極解決方案:
在當(dāng)前控制器的 - (void)viewDidDisappear:(BOOL)animated 里調(diào)用 [_timer invalidate]。
當(dāng) NSTimer 提前斷開和當(dāng)前控制器&runloop的強(qiáng)引用。
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
// 界面消失的時(shí)候,讓 timer 過期。
[_timer invalidate]; // 這個(gè)方法會(huì)斷開 timer 和 runloop 以及 當(dāng)前控制器之間的兩個(gè)強(qiáng)引用關(guān)系。
}
[圖片上傳失敗...(image-3c6521-1509969579725)]
關(guān)于 NSTimer 循環(huán)引用最后總結(jié)
只要在控制器中使用了 NSTimer,都可以在 - (void)viewDidDisappear:(BOOL)animated 里讓 NSTimer 斷開和當(dāng)前控制器的循環(huán)引用關(guān)系(如果有),以及一定斷開和 Runloop 的強(qiáng)引用關(guān)系。