如何解决Timer的循环引用

定时器Timer在开发过程中十分常见, 并不是所有使用Timer的地方都会产生循环引用,但是一旦产生就很难释放,我们平常使用Timer的姿态存在一些理所当然的错误,今天我们一起来纠正他.

Timer的使用方式

按照启动方式分为两种:

方式一:自启动

1
1
Timer.scheduledTimer(timeInterval: 2, target: self, selector: #selector(timerAction), userInfo: nil, repeats: true)

方式二:需要受到添加到runloop才会启动

1
2
3
Timer(timeInterval: 2, target: self, selector: #selector(timerAction), userInfo: nil, repeats: true)

RunLoop.current.add(timer2, forMode: .default)

按照事件回调方式又分为两种:

Block回调:

1
1
Timer.scheduledTimer(timeInterval: 2, target: self, selector: #selector(timerAction), userInfo: nil, repeats: true)

Target-action:

1
2
3
Timer.scheduledTimer(withTimeInterval: 2, repeats: true, block: { (timer) in
        DDLogInfo("timer")
    })

平常使用Timer的方式

强引用一个timer

1
var timer: Timer!

在viewDidLoad中初始化

1
2
3
4
5
6
 override func viewDidLoad() {
        super.viewDidLoad()
        timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true, block: { (timer) in
            DDLogInfo("timer")
        })
}

使用完之后再deinit中释放

1
2
3
4
1
2
3
4
deinit {
    timer.invalidate()
    timer = nil
}

看上去很完美, 实际使用过程中也没有任何问题, Timer也能被正常的释放.

但是问题来了, 我们使用的是 scheduledTimer 的方式初始化的Timer, 如果我们换成如下方式是否可行呢:

1
2
3
4
timer = Timer(timeInterval: 2, repeats: true, block: { (timer) in
            DDLogInfo("timer3")
        })
        RunLoop.current.add(timer3, forMode: .default)

咦!! 居然也可以,一切正常. 此时我们一般会草率的判断Timer的释放时机就是这样的. 知道有一天我们把初始化的代码写成下面的样子:

1
2
3
timer = Timer(timeInterval: 2, target: self, selector: #selector(timerAction), userInfo: nil, repeats: true)

RunLoop.current.add(timer, forMode: .default)

再或者这样子:

1
timer = Timer.scheduledTimer(timeInterval: 2, target: self, selector: #selector(timerAction), userInfo: nil, repeats: true)

你会发现,上面两种写法Timer是无法进行释放的. 细心的同学可能已经发现, 使用 block方式 传递事件的方式都能正确释放, 使用Target-action的方式响应事件的不能正确释放. 是的, 的确是这样, 因为使用Block时系统对 self做了弱引用处理, 所以不会产生循环引用, 但是Target-action方式却没有,故而产生循环引用, 既然产生了循环引用,那么 deinit方式也不会被调用, 所以Timer不会释放咯.

如何解决循环引用的问题

上面我们找到了循环引用的原因, 那么解决办法就会有很多, 有的同学第一反应就是: 那就都是用Block方式, 不使用Target-action. 这是个好方法, 但是限制了我的使用方式, 不舒服

又有同学会说, 那就叫在viewWillDisappear中释放timer, 问题如下:

页面即将消失的时候销毁timer单独看起来没有问题, 但是如果我们以后两个页面使用push方式切换时 A -> push -> B, 如果timerB中, 那么我们使用手势滑动popA时,会触发viewWillDisappear,此时Timer会被销毁,但是如果我们中途取消滑动,又回到B,那么Timer就位nil, 在使用Timer程序就会崩溃.除非我们相应的在viewWillAppear中再次创建Timer,但是不推荐此做法.

那么我们在viewDidDisappear中销毁Timer咋样呢, 也有问题:

还是A -> push -> B, 此时我们TimerA中, pushBAviewDidDisappear会被调用, 那么定时器被销毁, 当我们回到A时Timernil,调用Timer程序也会崩溃,除非我们相应的在viewWillAppear中再次创建Timer,但是我也不推荐此做法.

最好的释放Timer的方式

通过Target-action 方式Timer不能释放,是因为 Timer强引用的target, 也就是self. 所以我们可以新建一个类,用来初始化timer, 以及响应timer的时间, 然后通过block将事件的响应结果回调出去.这样还能将业务和UI分离,代码更加简洁易懂. 具体的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/// 防止timer循环引用
class HLLTimer: NSObject {
  //每次timer事件的回调
 var handler: ((Any?) -> Void)?

/// 初始化timer
func interval(timeInterval: TimeInterval, userInfo: Any?, repeats: Bool, completion:((Any?) -> Void)?) -> Timer {
    handler = completion
   let timer = Timer(timeInterval: timeInterval, target: self, selector: #selector(timerAction), userInfo: userInfo, repeats: repeats)
    RunLoop.current.add(timer, forMode: .default)
    return timer
}

/// tiemr事件
var count = 0
@objc func timerAction() {
    DDLogInfo("timer2")
    count += 1
    handler?(count)
}

deinit {
    DDLogInfo("hlltimer deinit")
} }

调用方式也十分简单:

1
2
3
timer = HLLTimer().interval(timeInterval: 2, userInfo: nil, repeats: true, completion: { (res) in
            DDLogInfo("\(res)")
        })

释放方式就能直接在deint中释放:

1
2
3
4
1
2
3
4
deinit {
    timer.invalidate()
    timer = nil
}