Timer 的循环引用
在使用 Timer 时,如果直接引用 self,会导致循环引用。示例代码:
class TimerExample {
var timer: Timer?
init() {
// Timer
// timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(observeTimeLine), userInfo: nil, repeats: true)
// 创建一个 Timer,并直接捕获 self
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
self.timerFired()
}
}
func timerFired() {
print("Timer fired")
}
deinit {
timer?.invalidate()
print("TimerExample deinitialized")
}
}
Timer对目标对象(如self)有一个强引用;
如果self对timer也有强引用,则形成循环引用,导致对象无法正常释放。
即使在 deinit 中这样写,试图手动销毁 timer,也是无效的。因为deinit根本不被调用!
deinit {
timer?.invalidate()
print("TimerExample deinitialized")
}
需要在销毁TimerExample实例前,手动调用timer?.invalidate()才行,不然因为实例被强持有不会触发deinit。
为什么 weak 修饰无效?
在某些场景下,开发者可能尝试用 weak 修饰 timer,以为可以解决循环引用问题:
class TimerExample {
weak var timer: Timer? // 尝试用 weak 修饰
init() {
// Timer
// timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(observeTimeLine), userInfo: nil, repeats: true)
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
self.timerFired()
}
}
func timerFired() {
print("Timer fired")
}
deinit {
timer?.invalidate()
print("TimerExample deinitialized")
}
}
这种情况下,weak 并不起作用,原因如下:
-
Timer被RunLoop强引用:- 当
Timer被注册到RunLoop后,RunLoop会对Timer保持一个强引用。 - 即使
self.timer是weak,RunLoop中的强引用仍然会让Timer存在,无法自动释放。
- 当
-
self闭包引用:- 即使
timer是weak,Timer的闭包仍然捕获了self的强引用。 - 这种隐式的强引用使得对象无法释放,导致循环引用。
- 即使
对于Timer.scheduledTimer 的 Handler 方式,[weak self] 可解循环引用
正确的做法是在 Timer 的闭包中使用 [weak self],避免直接引用 self,从而解决循环引用问题:
class TimerExample {
var timer: Timer?
init() {
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.timerFired()
}
}
func timerFired() {
print("Timer fired")
}
deinit {
timer?.invalidate()
print("TimerExample deinitialized")
}
}
闭包会捕获
self的弱引用;
当self被释放时,闭包中的self自动为nil,避免了循环引用问题。
总结
- 直接使用
Timer时,RunLoop的强引用使得weak修饰的timer无法释放。 -
解决循环引用的关键:在
Timer的闭包中捕获self的弱引用,或使用其他方法(如Proxy或DispatchSourceTimer)避免强引用问题。
使用 Proxy 解决循环引用
核心思想:通过引入一个中间对象(Proxy),Proxy 弱引用目标对象(self),从而避免 Timer 强引用 self。
// Proxy 类:
class TimerProxy {
weak var target: AnyObject?
init(target: AnyObject) {
self.target = target
}
@objc func timerFired(_ timer: Timer) {
(target as? TimerHandling)?.timerFired()
}
}
protocol TimerHandling: AnyObject {
func timerFired()
}
// 使用 Proxy 的类:
class TimerExample: TimerHandling {
private var timer: Timer?
init() {
let proxy = TimerProxy(target: self)
timer = Timer.scheduledTimer(timeInterval: 1.0,
target: proxy,
selector: #selector(TimerProxy.timerFired(_:)),
userInfo: nil,
repeats: true)
}
func timerFired() {
print("Timer fired")
}
deinit {
timer?.invalidate()
print("TimerExample deinitialized")
}
}
Timer持有TimerProxy的强引用,Proxy弱引用self。
当TimerExample被销毁时,TimerProxy的弱引用变为nil,不会引发循环引用。
使用 DispatchSourceTimer
核心思想:DispatchSourceTimer 不会被 RunLoop 持有,且可以灵活管理生命周期,通过捕获 self 的弱引用避免循环引用。
class TimerExample {
private var timer: DispatchSourceTimer?
init() {
// 创建一个 GCD 定时器
timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global())
timer?.schedule(deadline: .now(), repeating: 1.0) // 设置定时间隔
// 使用 [weak self] 避免循环引用
timer?.setEventHandler { [weak self] in
self?.timerFired()
}
timer?.resume() // 开始定时器
}
func timerFired() {
print("Timer fired")
}
deinit {
timer?.cancel() // 停止定时器
print("TimerExample deinitialized")
}
}
DispatchSourceTimer通过DispatchQueue管理,避免了RunLoop持有的问题。
self被弱引用,销毁时不会造成循环引用。
区别对比
| 特性 | Proxy | DispatchSourceTimer |
|---|---|---|
| 实现复杂度 | 较高,需要额外的类定义和协议支持 | 较低,直接使用 GCD 提供的 API |
| 性能 | 基于 RunLoop,受主线程影响 |
基于 GCD,适合多线程任务,性能更高 |
| 生命周期管理 | 必须手动销毁定时器(invalidate) |
手动 cancel 定时器即可 |
| 应用场景 | 适合需要与 RunLoop 交互的场景(如 UI 更新) |
适合后台任务、轻量级定时器场景 |
选择建议:
- 如果需要频繁更新 UI(如主线程计时器),使用 Proxy 更贴近 UIKit 风格。
- 如果是后台定时任务或性能优先场景,优先选择 DispatchSourceTimer。








网友评论