iOS Timer 的循環(huán)引用問題

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)有一個強引用
如果 selftimer 也有強引用,則形成循環(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 并不起作用,原因如下:

  1. TimerRunLoop 強引用:

    • Timer 被注冊到 RunLoop 后,RunLoop 會對 Timer 保持一個強引用。
    • 即使 self.timerweakRunLoop 中的強引用仍然會讓 Timer 存在,無法自動釋放。
  2. self 閉包引用:

    • 即使 timerweakTimer 的閉包仍然捕獲了 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 的弱引用,或使用其他方法(如 ProxyDispatchSourceTimer)避免強引用問題。

使用 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ù)、輕量級定時器場景

選擇建議:

  1. 如果需要頻繁更新 UI(如主線程計時器),使用 Proxy 更貼近 UIKit 風格。
  2. 如果是后臺定時任務(wù)或性能優(yōu)先場景,優(yōu)先選擇 DispatchSourceTimer。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容