關(guān)于 NSTimer 造成循環(huán)引用的問題

面試中,經(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í)行。

runloop 會(huì)強(qiáng)引用 NSTimer

當(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)單。


block 強(qiáng)引用控制器

解決辦法,使用經(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)引用。

問題出在哪?

  1. NSTimer & ViewController 雙向引用了。
  2. Runloop & NSTimer 單向引用了。
  3. 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)系。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容