前言

本文将从如何使用 NSTimerNSTimer 何种情况下会造成循环引用,以及如何避免循环引用几个角度进行介绍。

NSTimer 如何使用

A timer that fires after a certain time interval has elapsed, sending a specified message to a target object.

翻译:一种计时器,在经过一定的时间间隔后触发,向目标对象发送指定的消息。

创建

NSTimer 提供了三种创建方式:

  1. scheduledTimerWithTimeInterval 类方法开头的创建实例

  2. timerWithTimeInterval 开头的创建实例

  3. init 方法初始化方法创建实例

若采用第一种方式创建,会以 default mode 方式自动加入到当前的 RunLoop 中。

若采用第二、三种方式创建,需手动调用NSRunLoop对象的 addTimer:forMode: 方法。

let timer = Timer.init(timeInterval: 1.0, repeats: true) { timer in
   print("This")
}
RunLoop.main.add(timer, forMode: .default)

销毁

invalidate()

该方法是在其加入的 RunLoop 对象中移除timer的唯一方法,同时会 RunLoop 对象会移除其对对象的强引用,若配置了 targetuser info 对象, timer 也会移除对这些对象的强引用。

为何会造成循环引用

循环引用

考虑一个常见使用 NSTimer 的场景:在 ViewController 中将 timer 作为属性,而 timer 在创建采用了 target-action 的方式,根据 Apple 文档,timer 会对 target 产生强引用,这就产生了有向环,导致循环引用,下图是上例对象引用图:

image-20210817112104936

RunLoop持有timer

还是上例的场景,若在 timer 创建时不采用 target-action ,是不是就可以解决了?

确实可以解决,但还存在一个问题,由于 RunLoop 对象还持有着 timer 对象,这时 ViewController 能被正常释放,但 timer 的引用计数由于不为 1 ,无法被释放,这种情况只是 timer 无法被释放,并不算循环引用范畴,当然若 RunLoop 对象被释放了,则这个 timer 也会被释放掉。

image-20210817145841853

当然此上两种情况都是将 timerrepeat 参数设置为 true 时,若为 false 则在定时器第一次触发后,会自动失效,即 RunLoop 会移除对 timer 的强引用, timer 也会移除对 targetuser info 对象的强引用。

解决方案

合适的时机调用 invalidate()

在上文介绍了invalidate() , 我们只需要在合适的时机调用 invalidate()即可。

那么什么是合适的时机呢?

  1. 若清楚知道什么时候 timer 不再使用,则应立即调用。
  2. 若不清楚则应破除循环引用,最后在 ViewControllerdeinit 中进行调用。

针对循环引用,需破除有向环

  1. 采用 weak 关键字修饰 timer ,但需采用scheduledTimerWithTimeInterval 类方法开头的创建实例,因为这个方法会将 timer 加入到 RunLoop 对象中,否则由于 weak 修饰, timer 会被自动设为nil
  2. 采用 weak 关键字修饰 target 对象,但需在 block 中进行使用,原理大概是 block 中的 weakSelf 相当于一个临时变量,进而阻止了循环引用。
  3. 采用中介者模式,让其他对象承担 target 角色,从而阻止循环引用。
  4. 基于 NSProxy 方式,与 3 类似,通过其他对象来阻止循环引用,需注意的是 NSProxy 无法直接使用。
  5. 也可直接使用 Apple 提供的新 API
class func scheduledTimer(withTimeInterval interval: TimeInterval, repeats: Bool, block: @escaping (Timer) -> Void) -> Timer

init(timeInterval interval: TimeInterval, repeats: Bool, block: @escaping (Timer) -> Void)

convenience init(fire date: Date, interval: TimeInterval, repeats: Bool, block: @escaping (Timer) -> Void)

the timer itself is passed as the parameter to this block when executed to aid in avoiding cyclical references。

翻译:在执行时,定时器本身作为参数传递给这个块,以帮助避免循环引用。

引用

iOS之NSTimer循环引用的解决方案

iOS中Timer循环引用原因及解决方案

Apple文档 - NSTimer