面試中,經(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í)行粤咪。
當(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)單很洋。
解決辦法,使用經(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)題出在哪滑臊?
- NSTimer & ViewController 雙向引用了口芍。
- Runloop & NSTimer 單向引用了。
- 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)系。