面試中,經常會問道 NSTimer 循環引用的問題。
閑話少敘。下面來講講 NSTimer 為什么會造成循環引用?
使用 NSTimer 的 block 的方式來創建定時器。
一般情況下,我們會把 NSTimer 定義成當前控制器的一個屬性/成員變量。
@implementation ViewController {
NSTimer *_timer;
}
到此步為止,造成了一個單向引用
ViewController -> NSTimer
使用 NSTimer 的 block 語法,來創建定時器任務。
- (void)blockTimer {
// 創建一個 _timer。
_timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
// 此控制器的內部并沒有捕獲控制器,于是就沒有造成對控制器的強引用。
NSLog(@"%@",@"timer 開始執行。");
}];
[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSDefaultRunLoopMode];
由于定時器執行任務的 block,并沒有引用 self,到次為止,引用關系仍然是單向的。
重寫 dealloc 方法來檢測,當前控制器是否可以被銷毀。
- (void)dealloc {
NSLog(@"%@",@"控制器不能被銷毀?");
}
運行結果:
2017-11-06 19:14:50.195 CodeForNSTimerRetainCycle[18912:19230566] timer 開始執行。
2017-11-06 19:14:51.196 CodeForNSTimerRetainCycle[18912:19230566] timer 開始執行。
2017-11-06 19:14:52.196 CodeForNSTimerRetainCycle[18912:19230566] timer 開始執行。
2017-11-06 19:14:53.196 CodeForNSTimerRetainCycle[18912:19230566] timer 開始執行。
timer 可以正常執行。
當點擊了返回按鈕,退出此控制器的 Log 輸出。
2017-11-06 19:15:36.344 CodeForNSTimerRetainCycle[18949:19234509] 控制器不能被銷毀?
2017-11-06 19:15:36.482 CodeForNSTimerRetainCycle[18949:19234509] timer 開始執行。
2017-11-06 19:15:37.483 CodeForNSTimerRetainCycle[18949:19234509] timer 開始執行。
2017-11-06 19:15:38.482 CodeForNSTimerRetainCycle[18949:19234509] timer 開始執行。
2017-11-06 19:15:39.482 CodeForNSTimerRetainCycle[18949:19234509] timer 開始執行。
控制器可以正常銷毀。但是 NSTimer 仍然在繼續執行。
當點擊控制器的返回按鈕時,圖中那條白色的箭頭斷開,當前控制器又沒有其他的對象引用(除了 navigationController,但pop 的時候,這條連接已經斷開了)。所以,控制器可以被正確釋放。
2017-11-06 19:15:36.344 CodeForNSTimerRetainCycle[18949:19234509] 控制器不能被銷毀?
NSTimer 沒有被釋放,是因為,當我們把 NSTimer 添加到運行循環時,運行循環強引用了這個 NSTimer(也就是圖中紅色的箭頭)。
而 Runloop 是無法銷毀的(UI線程)。
所以,這條紅色的箭頭,就無法斷開。NSTimer 不能被釋放。
于是就出現了控制器被正確銷毀,但是在控制器里創建的 NSTimer 卻可以仍然運行的情況。
是 Runloop 強引用了這個 NSTimer。
但這種寫法,并不阻礙當前控制器的釋放。
但為了保證,NSTimer 在控制器釋放的時候,就不要在繼續執行了,可以在 dealloc 方法里,讓 NSTimer 過期。
- (void)dealloc {
NSLog(@"%@",@"控制器不能被銷毀?");
[_timer invalidate];
}
使用 NSTimer 的 block 的方式來創建定時器,在 block 內部訪問 self。
在 block 的內部訪問 self,肯定會造成循環引用,道理也很簡單。
解決辦法,使用經典的 weak-strong dance
即可。
- (void)blockTimer {
// 創建一個 _timer。
__weak typeof(self) weakSelf = self;
_timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
__strong typeof(weakSelf) strongSelf = weakSelf;
// 此控制器的內部并沒有捕獲控制器,于是就沒有造成對控制器的強引用。
// NSLog(@"%@",@"timer 開始執行。");
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];
運行結果:
2017-11-06 19:33:36.518 CodeForNSTimerRetainCycle[19195:19304854] 控制器不能被銷毀?
控制器可以正常退出,NSTimer 也不會繼續執行了。
使用 NSTimer 的 target 方式來來創建定時任務。
使用NSTimer 的 target 方式來來創建定時任務。NSTimer 一定會強引用住當前控制器。
#pragma mark timer 的 target 方式
- (void)targetTimer {
// 這種方式創建,timer 會強引用 self。導致這個控制器無法被銷毀。
_timer = [[NSTimer alloc] initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:1] interval:1 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
}
運行結果:
2017-11-06 19:35:56.565 CodeForNSTimerRetainCycle[19241:19313152] timer 事件執行!
2017-11-06 19:35:57.565 CodeForNSTimerRetainCycle[19241:19313152] timer 事件執行!
2017-11-06 19:35:58.565 CodeForNSTimerRetainCycle[19241:19313152] timer 事件執行!
當pop 當前控制器的時候,dealloc 方法也沒有被執行。說明當前控制器沒有被釋放。
為什么使用了 target 的方式來NSTimer 定時任務的時候,當前控制器不能被釋放呢?
因為 NSTimer 強引用了這個控制器。
為什么 NSTimer 通過 target 的方式創建定時任務的時候,要強引用這個控制器呢?
NSTimer 的任務來源的 SEL 來自于這個控制器,只有在當前控制器上才能通過 SEL 找到方法的實現。如果 控制器不被強引用住,那么會會影響 NSTimer 定時任務的執行。
所以,為了保證 NSTimer 任務能夠定時的執行,就必須強引用這個控制器。
也就是說,NSTimer 內部有一個 __strong target
,強引用了這個控制器,于是就造成了循環引用。
兩個對象,如果雙發都被強引用了,一般 strong,一般 weak 即可。
但是 NSTimer 是系統的類,且當前控制器的傳遞是使用 target
的方式。又不是我們自己定義類,把屬性改成 weak 修飾就好了。
幸好的是,NSTimer 提供了一個可以斷開和當前 target 強引用關系的方法。
[_timer invalidate];
當我們調用這個方法的時候。
- NSTimer 會斷開自己和 target 的強引用關系。
- Runloop 會斷開自己和 NSTimer 的強引用關系。
第一條解決了控制器釋放的問題。
第二條解決了在控制器釋放之后,NSTimer仍然在繼續執行的問題。
于是就開心的在當前控制器的 dealloc 方法里加了這么一行代碼。
- (void)dealloc {
NSLog(@"%@",@"控制器不能被銷毀?");
[_timer invalidate];
}
運行結果:
2017-11-06 19:43:58.682 CodeForNSTimerRetainCycle[19305:19325055] timer 事件執行!
2017-11-06 19:43:59.682 CodeForNSTimerRetainCycle[19305:19325055] timer 事件執行!
2017-11-06 19:44:00.682 CodeForNSTimerRetainCycle[19305:19325055] timer 事件執行!
2017-11-06 19:44:01.682 CodeForNSTimerRetainCycle[19305:19325055] timer 事件執行!
發現等控制器pop 出的時候,NSTimer 既沒斷開和控制器的強引用,也沒有被 Runloop 斷開對自身的強引用。
問題出在哪?
- NSTimer & ViewController 雙向引用了。
- Runloop & NSTimer 單向引用了。
- NSTimer invalidate 方法寫在了 ViewController 的 dealloc 方法里了。
答案呼之欲出了:
由于第一條,NSTimer & ViewController 雙向引用了,當前控制器根本無法被釋放。
從而無法執行到 dealloc , 就無法執行 [_timer invalidate] 這個方法。
但這個方法,又是 NSTimer 斷開和控制器以及 runloop 的核心方法。
所以,就造成了無法控制器無法釋放,以及 NSTimer 仍然在繼續執行的問題。
終極解決方案:
在當前控制器的 - (void)viewDidDisappear:(BOOL)animated
里調用 [_timer invalidate]
。
當 NSTimer 提前斷開和當前控制器&runloop的強引用。
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
// 界面消失的時候,讓 timer 過期。
[_timer invalidate]; // 這個方法會斷開 timer 和 runloop 以及 當前控制器之間的兩個強引用關系。
}
[圖片上傳失敗...(image-3c6521-1509969579725)]
關于 NSTimer 循環引用最后總結
只要在控制器中使用了 NSTimer,都可以在 - (void)viewDidDisappear:(BOOL)animated
里讓 NSTimer 斷開和當前控制器的循環引用關系(如果有),以及一定斷開和 Runloop 的強引用關系。