關于 NSTimer 造成循環引用的問題

面試中,經常會問道 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 仍然在繼續執行。

runloop 會強引用 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,肯定會造成循環引用,道理也很簡單。


block 強引用控制器

解決辦法,使用經典的 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 斷開對自身的強引用。

問題出在哪?

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,923評論 6 535
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,740評論 3 420
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,856評論 0 380
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,175評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,931評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,321評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,383評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,533評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,082評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,891評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,618評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,319評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,732評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,987評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,794評論 3 394
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,076評論 2 375

推薦閱讀更多精彩內容