一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第21天,点击查看活动详情。
前言
在iOS开发中,我们使用定时器(timer)的几率很高,系统中最常用的方式有GCD中提供的timer接口和我们今天要讨论的NSTimer。关于GCD相关的接口,我们今天不讨论,我们接下来看看NSTimer.
对于NSTimer,我们都知道这家伙最让人深恶痛绝的,肯定就是它容易引起循环引用,造成内存泄漏,甚至在执行定时任务的时候导致crash。
我们接下将会分析NSTimer是如何引起循环引用,并给出几种方案来破开这种循环引用状态。
NSTimer造成内存泄漏的原因
我们查看scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:和timerWithTimeInterval:target:selector:userInfo:repeats:的接口文档,可以看到对target的有这样的一句解释:
The timer maintains a strong reference to target until it (the timer) is invalidated.
哦豁!NSTimer会对target进行强持有,如果这个时候作为target的对象(例如我们最常见的情况:UIViewController)强持有NSTimer,这就引起了循环引用。
既然引起了循环引用,那么在破开这个引用环之前,NSTimer和target都无法被释放,这就很合理了。
既然这样,我们就破开引用环。我们在必要的时候在target中将NSTimer置为nil。这样是否就可以释放NSTimer了呢?答案是:不行。有兴趣的同学可以试试,在NSTimer的selector中加一条打印,我们会发现这该死的打印根本不会自己停,直到你因为再也不想看到它而关掉调试为止。
在我们心态爆炸,一脸懵逼的时候,我们再看看官方文档对NSTimer的一段介绍:
Timers work in conjunction with run loops. Run loops maintain strong references to their timers, so you don’t have to maintain your own strong reference to a timer after you have added it to a run loop.
这段话提到了一个关键点:在我们将NSTimer加入到RunLoop中之后,RunLoop就会维持对NSTimer的强引用。这也是不管我们是否在控制器中对NSTimer进行强引用,在我们没有将NSTimer进行invalidate之前,NSTimer都无法被释放的原因了。
说到invalidate,我们看看官方文档对这个方法的介绍:
Stops the timer from ever firing again and requests its removal from its run loop.
我们可以看到invalidate方法做了两个事情:1.停止了NSTimer的定时事件。2.将NSTimer从RunLoop中移除。
看到这里是否是恍然大悟,原来NSTimer得已被释放,最大的功臣是invalidate方法,如果没有它,我们想从RunLoop中移除NSTimer可就是个麻烦的事了。
所以如果要释放NSTimer,我么必须做到两件事:1.打破NSTimer和target的引用环。2.将NSTimer从RunLoop中移除,即调用NSTimer的invalidate方法。