NSTimer 引起 target 不能释放
NSTimer 是 RunLoop 的一种源,这就注定了其使用过程中与其他类的不同。
1 | - (NSTimer * _Nonnull)addTimer { |
上面这个代码是在控制器中加入了 Timer,然后执行 testOne 方法,repeat 为 YES。表现形式就是,当进入这个控制器之后,testOne 方法会在 1 秒间隔重复执行。如果没有其他 timer 有关的方法,那么当控制器 dimiss 之后,这个控制器不会被销毁,testOne 方法还是会一直被调用。
处理方式一, 调用 invalidate
出现上面的问题的原因是:
NSTimer 通过 scheduleTimerWithTimeInterval: target: selector: userInfo: repeats: 这个方法创建 timer 之后,这个 timer 会被添加到 runloop 的中,同时,runloop 会对 timer 做一次引用,而 target: 中的 self 又会被 timer 引用一次。runloop 是一直在内存中引用着 timer 的,所以 self 会被一直持有,所以就不会被释放了。
NSTimer 为我们提供了一个将 timer 从它的 runloop 中移除的方法:
1 | - (void)invalidate; |
这里指出了 invalidate 是唯一一种移除的方式 … 所以一般的处理方式是在不用到 timer 的时候让其调用 invalidate 这个方法,比如上面的问题我们如下处理:
1 | TimerVC { |
step:
- 首先在当前类中添加全局变量,在 addTimer 的时候为其赋值
- 在该类不在使用该 timer 的时候,[_timer invalidate] 和 _timer = nil
这样就可以避免当前类不释放的问题了。但是 … 有没有觉得很不优雅,当前控制器需要在自己离开的时候照顾这个 runloop 家的宠儿的问题,而且为了它的消亡添加了一个全局变量
处理方式二:代理对象
其实,这里的问题就出在 runloop -> timer -> self 这个链中,因为 runloop 的不死,导致了 self 一直被引用。上面的做法是强行将 runloop -> timer 的应用切断。如果有其他方案的话,我们只能去想 timer -> self 这条链了,因为 runloop 是不能死掉的。
首先,为 timer 添加一个类目:
1 | NSTimer (Weak) |
其次,是在这类目中添加一个 target 类
1 | NSTimerTarget:NSObject |
接下来看一下实现:
1 |
|
在这里,通过一个中间类将外部调用 timer 的类换成了这个中间类,这样 timer 对象就不会持有调用了 NSTimer 方法的对象了,除此之外,这里 target 类还需要将外部类的 selector 方重新再 targetSelector 中调用。
targetSelector 这个方法在控制器(或者其他写了 NSTimer 启动方法的类)销毁之后其实还会继续调用,所以在内部判断移除 timer 的时机。
这样,整体上我们就不需要对外部调用 timer 的类做一些特殊的处理了。
总结:
其实,网上还有其他的解决方式,后续会继续看这个问题:
- 通过 GCD 实现 timer, NSWeakTimer
- 通过代理类 实现 timer,HWWeakTimer
第一种方式,是彻底将 timer 替换成了 gcd 中的 timer;第二种方式和目前的实现方式类似,但是是引入了新的 timer 类。
其实,这个问题应该是遇到很久了,大部分都是直接将 timer 移除的方法耦合到了使用的类中,一直没有去深究这问题,这次深究完了发现:
- NSRunloop 的理解加深
- 代码的设计上要多动脑
- 遇到问题多考虑集中解决方案然后再继续写代码
- 代码的优化方式永远不能止步
最后,代码传送带。