Timer 的循環(huán)引用
在使用 Timer 時,如果直接引用 self,會導致循環(huán)引用。示例代碼:
class TimerExample {
var timer: Timer?
init() {
// Timer
// timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(observeTimeLine), userInfo: nil, repeats: true)
// 創(chuàng)建一個 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也有強引用,則形成循環(huán)引用,導致對象無法正常釋放。
即使在 deinit 中這樣寫,試圖手動銷毀 timer,也是無效的。因為deinit根本不被調(diào)用!
deinit {
timer?.invalidate()
print("TimerExample deinitialized")
}
需要在銷毀TimerExample實例前,手動調(diào)用timer?.invalidate()才行,不然因為實例被強持有不會觸發(fā)deinit。
為什么 weak 修飾無效?
在某些場景下,開發(fā)者可能嘗試用 weak 修飾 timer,以為可以解決循環(huán)引用問題:
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的強引用。 - 這種隱式的強引用使得對象無法釋放,導致循環(huán)引用。
- 即使
對于Timer.scheduledTimer 的 Handler 方式,[weak self] 可解循環(huán)引用
正確的做法是在 Timer 的閉包中使用 [weak self],避免直接引用 self,從而解決循環(huán)引用問題:
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,避免了循環(huán)引用問題。
總結(jié)
- 直接使用
Timer時,RunLoop的強引用使得weak修飾的timer無法釋放。 -
解決循環(huán)引用的關(guān)鍵:在
Timer的閉包中捕獲self的弱引用,或使用其他方法(如Proxy或DispatchSourceTimer)避免強引用問題。
使用 Proxy 解決循環(huán)引用
核心思想:通過引入一個中間對象(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的弱引用變?yōu)?nil,不會引發(fā)循環(huán)引用。
使用 DispatchSourceTimer
核心思想:DispatchSourceTimer 不會被 RunLoop 持有,且可以靈活管理生命周期,通過捕獲 self 的弱引用避免循環(huán)引用。
class TimerExample {
private var timer: DispatchSourceTimer?
init() {
// 創(chuàng)建一個 GCD 定時器
timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global())
timer?.schedule(deadline: .now(), repeating: 1.0) // 設(shè)置定時間隔
// 使用 [weak self] 避免循環(huán)引用
timer?.setEventHandler { [weak self] in
self?.timerFired()
}
timer?.resume() // 開始定時器
}
func timerFired() {
print("Timer fired")
}
deinit {
timer?.cancel() // 停止定時器
print("TimerExample deinitialized")
}
}
DispatchSourceTimer通過DispatchQueue管理,避免了RunLoop持有的問題。
self被弱引用,銷毀時不會造成循環(huán)引用。
區(qū)別對比
| 特性 | Proxy | DispatchSourceTimer |
|---|---|---|
| 實現(xiàn)復雜度 | 較高,需要額外的類定義和協(xié)議支持 | 較低,直接使用 GCD 提供的 API |
| 性能 | 基于 RunLoop,受主線程影響 |
基于 GCD,適合多線程任務(wù),性能更高 |
| 生命周期管理 | 必須手動銷毀定時器(invalidate) |
手動 cancel 定時器即可 |
| 應用場景 | 適合需要與 RunLoop 交互的場景(如 UI 更新) |
適合后臺任務(wù)、輕量級定時器場景 |
選擇建議:
- 如果需要頻繁更新 UI(如主線程計時器),使用 Proxy 更貼近 UIKit 風格。
- 如果是后臺定時任務(wù)或性能優(yōu)先場景,優(yōu)先選擇 DispatchSourceTimer。