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。