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

面試中,經(jīng)常會(huì)問(wèn)道 NSTimer 循環(huán)引用的問(wèn)題津滞。
閑話(huà)少敘。下面來(lái)講講 NSTimer 為什么會(huì)造成循環(huán)引用?

使用 NSTimer 的 block 的方式來(lái)創(chuàng)建定時(shí)器喇勋。

一般情況下树枫,我們會(huì)把 NSTimer 定義成當(dāng)前控制器的一個(gè)屬性/成員變量。

@implementation ViewController {
    NSTimer *_timer;
}

到此步為止,造成了一個(gè)單向引用

ViewController -> NSTimer

使用 NSTimer 的 block 語(yǔ)法年叮,來(lái)創(chuàng)建定時(shí)器任務(wù)。

- (void)blockTimer {
    // 創(chuàng)建一個(gè) _timer玻募。
    _timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        // 此控制器的內(nèi)部并沒(méi)有捕獲控制器只损,于是就沒(méi)有造成對(duì)控制器的強(qiáng)引用。
        NSLog(@"%@",@"timer 開(kāi)始執(zhí)行七咧。");
    }];

    [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSDefaultRunLoopMode];

由于定時(shí)器執(zhí)行任務(wù)的 block跃惫,并沒(méi)有引用 self,到次為止艾栋,引用關(guān)系仍然是單向的爆存。

重寫(xiě) dealloc 方法來(lái)檢測(cè),當(dāng)前控制器是否可以被銷(xiāo)毀蝗砾。

- (void)dealloc {
    NSLog(@"%@",@"控制器不能被銷(xiāo)毀终蒂?");
}

運(yùn)行結(jié)果:

2017-11-06 19:14:50.195 CodeForNSTimerRetainCycle[18912:19230566] timer 開(kāi)始執(zhí)行。
2017-11-06 19:14:51.196 CodeForNSTimerRetainCycle[18912:19230566] timer 開(kāi)始執(zhí)行遥诉。
2017-11-06 19:14:52.196 CodeForNSTimerRetainCycle[18912:19230566] timer 開(kāi)始執(zhí)行拇泣。
2017-11-06 19:14:53.196 CodeForNSTimerRetainCycle[18912:19230566] timer 開(kāi)始執(zhí)行。

timer 可以正常執(zhí)行矮锈。

當(dāng)點(diǎn)擊了返回按鈕霉翔,退出此控制器的 Log 輸出。

2017-11-06 19:15:36.344 CodeForNSTimerRetainCycle[18949:19234509] 控制器不能被銷(xiāo)毀苞笨?
2017-11-06 19:15:36.482 CodeForNSTimerRetainCycle[18949:19234509] timer 開(kāi)始執(zhí)行债朵。
2017-11-06 19:15:37.483 CodeForNSTimerRetainCycle[18949:19234509] timer 開(kāi)始執(zhí)行。
2017-11-06 19:15:38.482 CodeForNSTimerRetainCycle[18949:19234509] timer 開(kāi)始執(zhí)行瀑凝。
2017-11-06 19:15:39.482 CodeForNSTimerRetainCycle[18949:19234509] timer 開(kāi)始執(zhí)行序芦。

控制器可以正常銷(xiāo)毀。但是 NSTimer 仍然在繼續(xù)執(zhí)行粤咪。

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

當(dāng)點(diǎn)擊控制器的返回按鈕時(shí)谚中,圖中那條白色的箭頭斷開(kāi),當(dāng)前控制器又沒(méi)有其他的對(duì)象引用(除了 navigationController,但pop 的時(shí)候寥枝,這條連接已經(jīng)斷開(kāi)了)宪塔。所以,控制器可以被正確釋放囊拜。

2017-11-06 19:15:36.344 CodeForNSTimerRetainCycle[18949:19234509] 控制器不能被銷(xiāo)毀某筐?

NSTimer 沒(méi)有被釋放,是因?yàn)楣邗危?dāng)我們把 NSTimer 添加到運(yùn)行循環(huán)時(shí)南誊,運(yùn)行循環(huán)強(qiáng)引用了這個(gè) NSTimer(也就是圖中紅色的箭頭)身诺。
而 Runloop 是無(wú)法銷(xiāo)毀的(UI線(xiàn)程)。
所以抄囚,這條紅色的箭頭霉赡,就無(wú)法斷開(kāi)。NSTimer 不能被釋放怠苔。
于是就出現(xiàn)了控制器被正確銷(xiāo)毀同廉,但是在控制器里創(chuàng)建的 NSTimer 卻可以仍然運(yùn)行的情況仪糖。

是 Runloop 強(qiáng)引用了這個(gè) NSTimer柑司。

但這種寫(xiě)法,并不阻礙當(dāng)前控制器的釋放锅劝。

但為了保證攒驰,NSTimer 在控制器釋放的時(shí)候,就不要在繼續(xù)執(zhí)行了故爵,可以在 dealloc 方法里玻粪,讓 NSTimer 過(guò)期。

- (void)dealloc {
    NSLog(@"%@",@"控制器不能被銷(xiāo)毀诬垂?");
    [_timer invalidate];
}

使用 NSTimer 的 block 的方式來(lái)創(chuàng)建定時(shí)器,在 block 內(nèi)部訪(fǎng)問(wèn) self劲室。

在 block 的內(nèi)部訪(fǎng)問(wèn) 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)部并沒(méi)有捕獲控制器,于是就沒(méi)有造成對(duì)控制器的強(qiáng)引用官脓。
        // NSLog(@"%@",@"timer 開(kāi)始執(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] 控制器不能被銷(xiāo)毀?

控制器可以正常退出卑笨,NSTimer 也不會(huì)繼續(xù)執(zhí)行了孕暇。

使用 NSTimer 的 target 方式來(lái)來(lái)創(chuàng)建定時(shí)任務(wù)。

使用NSTimer 的 target 方式來(lái)來(lái)創(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è)控制器無(wú)法被銷(xiāo)毀铛楣。
    _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 方法也沒(méi)有被執(zhí)行。說(shuō)明當(dāng)前控制器沒(méi)有被釋放岸浑。

為什么使用了 target 的方式來(lái)NSTimer 定時(shí)任務(wù)的時(shí)候搏存,當(dāng)前控制器不能被釋放呢?

因?yàn)?NSTimer 強(qiáng)引用了這個(gè)控制器矢洲。

為什么 NSTimer 通過(guò) target 的方式創(chuàng)建定時(shí)任務(wù)的時(shí)候璧眠,要強(qiáng)引用這個(gè)控制器呢?

NSTimer 的任務(wù)來(lái)源的 SEL 來(lái)自于這個(gè)控制器读虏,只有在當(dāng)前控制器上才能通過(guò) SEL 找到方法的實(shí)現(xiàn)责静。如果 控制器不被強(qiáng)引用住,那么會(huì)會(huì)影響 NSTimer 定時(shí)任務(wù)的執(zhí)行盖桥。

所以灾螃,為了保證 NSTimer 任務(wù)能夠定時(shí)的執(zhí)行,就必須強(qiáng)引用這個(gè)控制器揩徊。

也就是說(shuō)腰鬼,NSTimer 內(nèi)部有一個(gè) __strong target,強(qiáng)引用了這個(gè)控制器塑荒,于是就造成了循環(huán)引用熄赡。

兩個(gè)對(duì)象,如果雙發(fā)都被強(qiáng)引用了齿税,一般 strong彼硫,一般 weak 即可。

但是 NSTimer 是系統(tǒng)的類(lèi)偎窘,且當(dāng)前控制器的傳遞是使用 target 的方式乌助。又不是我們自己定義類(lèi),把屬性改成 weak 修飾就好了陌知。

幸好的是他托,NSTimer 提供了一個(gè)可以斷開(kāi)和當(dāng)前 target 強(qiáng)引用關(guān)系的方法。

[_timer invalidate];

當(dāng)我們調(diào)用這個(gè)方法的時(shí)候仆葡。

  • NSTimer 會(huì)斷開(kāi)自己和 target 的強(qiáng)引用關(guān)系赏参。
  • Runloop 會(huì)斷開(kāi)自己和 NSTimer 的強(qiáng)引用關(guān)系。

第一條解決了控制器釋放的問(wèn)題沿盅。
第二條解決了在控制器釋放之后,NSTimer仍然在繼續(xù)執(zhí)行的問(wèn)題把篓。

于是就開(kāi)心的在當(dāng)前控制器的 dealloc 方法里加了這么一行代碼。

- (void)dealloc {
    NSLog(@"%@",@"控制器不能被銷(xiāo)毀腰涧?");
    [_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 既沒(méi)斷開(kāi)和控制器的強(qiáng)引用坊谁,也沒(méi)有被 Runloop 斷開(kāi)對(duì)自身的強(qiáng)引用。

問(wèn)題出在哪滑臊?

  1. NSTimer & ViewController 雙向引用了口芍。
  2. Runloop & NSTimer 單向引用了。
  3. NSTimer invalidate 方法寫(xiě)在了 ViewController 的 dealloc 方法里了雇卷。

答案呼之欲出了:

由于第一條鬓椭,NSTimer & ViewController 雙向引用了,當(dāng)前控制器根本無(wú)法被釋放关划。
從而無(wú)法執(zhí)行到 dealloc , 就無(wú)法執(zhí)行 [_timer invalidate] 這個(gè)方法小染。
但這個(gè)方法,又是 NSTimer 斷開(kāi)和控制器以及 runloop 的核心方法祭玉。
所以氧映,就造成了無(wú)法控制器無(wú)法釋放春畔,以及 NSTimer 仍然在繼續(xù)執(zhí)行的問(wèn)題脱货。

終極解決方案:

在當(dāng)前控制器的 - (void)viewDidDisappear:(BOOL)animated 里調(diào)用 [_timer invalidate]
當(dāng) NSTimer 提前斷開(kāi)和當(dāng)前控制器&runloop的強(qiáng)引用律姨。

- (void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];
    // 界面消失的時(shí)候振峻,讓 timer 過(guò)期。
    [_timer invalidate]; // 這個(gè)方法會(huì)斷開(kāi) timer 和 runloop 以及 當(dāng)前控制器之間的兩個(gè)強(qiáng)引用關(guān)系择份。
}

[圖片上傳失敗...(image-3c6521-1509969579725)]


關(guān)于 NSTimer 循環(huán)引用最后總結(jié)

只要在控制器中使用了 NSTimer扣孟,都可以在 - (void)viewDidDisappear:(BOOL)animated 里讓 NSTimer 斷開(kāi)和當(dāng)前控制器的循環(huán)引用關(guān)系(如果有),以及一定斷開(kāi)和 Runloop 的強(qiáng)引用關(guān)系。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末荣赶,一起剝皮案震驚了整個(gè)濱河市凤价,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌拔创,老刑警劉巖利诺,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異剩燥,居然都是意外死亡慢逾,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門(mén)灭红,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)侣滩,“玉大人,你說(shuō)我怎么就攤上這事变擒【椋” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵娇斑,是天一觀的道長(zhǎng)策添。 經(jīng)常有香客問(wèn)我澈段,道長(zhǎng),這世上最難降的妖魔是什么舰攒? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任败富,我火速辦了婚禮,結(jié)果婚禮上摩窃,老公的妹妹穿的比我還像新娘兽叮。我一直安慰自己,他們只是感情好猾愿,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布鹦聪。 她就那樣靜靜地躺著,像睡著了一般蒂秘。 火紅的嫁衣襯著肌膚如雪泽本。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,292評(píng)論 1 301
  • 那天姻僧,我揣著相機(jī)與錄音规丽,去河邊找鬼。 笑死撇贺,一個(gè)胖子當(dāng)著我的面吹牛赌莺,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播松嘶,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼艘狭,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了翠订?” 一聲冷哼從身側(cè)響起巢音,我...
    開(kāi)封第一講書(shū)人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎尽超,沒(méi)想到半個(gè)月后官撼,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡橙弱,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年歧寺,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片棘脐。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡斜筐,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蛀缝,到底是詐尸還是另有隱情顷链,我是刑警寧澤,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布屈梁,位于F島的核電站嗤练,受9級(jí)特大地震影響榛了,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜煞抬,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一霜大、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧革答,春花似錦战坤、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至溪食,卻和暖如春囊卜,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背错沃。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工栅组, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人捎废。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓笑窜,卻偏偏與公主長(zhǎng)得像致燥,于是被迫代替她去往敵國(guó)和親登疗。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容