定时器Timer在开发过程中十分常见, 并不是所有使用Timer的地方都会产生循环引用,但是一旦产生就很难释放,我们平常使用Timer的姿态存在一些理所当然的错误,今天我们一起来纠正他.
Timer的使用方式
按照启动方式分为两种:
方式一:自启动
Timer.scheduledTimer(timeInterval: 2, target: self, selector: #selector(timerAction), userInfo: nil, repeats: true)
方式二:需要受到添加到runloop才会启动
Timer(timeInterval: 2, target: self, selector: #selector(timerAction), userInfo: nil, repeats: true)
RunLoop.current.add(timer2, forMode: .default)
按照事件回调方式又分为两种:
Block回调:
Timer.scheduledTimer(timeInterval: 2, target: self, selector: #selector(timerAction), userInfo: nil, repeats: true)
Target-action:
Timer.scheduledTimer(withTimeInterval: 2, repeats: true, block: { (timer) in
DDLogInfo("timer")
})
平常使用Timer的方式
强引用一个timer
var timer: Timer!
在viewDidLoad中初始化
override func viewDidLoad() {
super.viewDidLoad()
timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true, block: { (timer) in
DDLogInfo("timer")
})
}
使用完之后再deinit中释放
deinit {
timer.invalidate()
timer = nil
}
看上去很完美, 实际使用过程中也没有任何问题, Timer也能被正常的释放.
但是问题来了, 我们使用的是 scheduledTimer 的方式初始化的Timer, 如果我们换成如下方式是否可行呢:
timer = Timer(timeInterval: 2, repeats: true, block: { (timer) in
DDLogInfo("timer3")
})
RunLoop.current.add(timer3, forMode: .default)
咦!! 居然也可以,一切正常. 此时我们一般会草率的判断Timer的释放时机就是这样的. 知道有一天我们把初始化的代码写成下面的样子:
timer = Timer(timeInterval: 2, target: self, selector: #selector(timerAction), userInfo: nil, repeats: true)
RunLoop.current.add(timer, forMode: .default)
再或者这样子:
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
, 如果timer
在B
中, 那么我们使用手势滑动pop
到A
时,会触发viewWillDisappear
,此时Timer会被销毁,但是如果我们中途取消滑动,又回到B
,那么Timer
就位nil
, 在使用Timer程序就会崩溃.除非我们相应的在viewWillAppear
中再次创建Timer
,但是不推荐此做法.
那么我们在viewDidDisappear中销毁Timer咋样呢, 也有问题:
还是A -> push -> B
, 此时我们Timer
在A
中, push
到B
后A
的viewDidDisappear
会被调用, 那么定时器被销毁, 当我们回到A时Timer
为nil
,调用Timer
程序也会崩溃,除非我们相应的在viewWillAppear
中再次创建Timer
,但是我也不推荐此做法.
最好的释放Timer的方式
通过Target-action
方式Timer
不能释放,是因为 Timer
强引用的target
, 也就是self
. 所以我们可以新建一个类,用来初始化timer
, 以及响应timer的时间, 然后通过block
将事件的响应结果回调出去.这样还能将业务和UI
分离,代码更加简洁易懂. 具体的实现如下:
/// 防止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")
}
}
调用方式也十分简单:
timer = HLLTimer().interval(timeInterval: 2, userInfo: nil, repeats: true, completion: { (res) in
DDLogInfo("\(res)")
})
释放方式就能直接在deint中释放:
deinit {
timer.invalidate()
timer = nil
}