You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Swift Timer二次启动不触发问题及线程同步方案咨询

Timer二次启动后不触发的原因及解决方法

问题描述

我写了一段Swift代码用于启停Timer:

private var timer: Timer? = nil

private func startTimer() {
    if (timer != nil) {
        return
    }
    logger.error("Starting timer on \(Thread.current)")
    timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { timer in
        self.logger.error("Timer fired on \(Thread.current)")
    })
}

private func stopTimer() {
    if (timer == nil) {
        return
    }
    logger.error("Stopping timer on \(Thread.current)")
    timer?.invalidate()
    timer = nil
}

这段代码多次启停Timer时,首次正常,但第二次启动后Timer完全不触发。以下是两次运行的日志:

Run 1

Starting timer on <_NSMainThread: 0x2829e4140>{number = 1, name = main}
Timer fired on <_NSMainThread: 0x2829e4140>{number = 1, name = main}
Timer fired on <_NSMainThread: 0x2829e4140>{number = 1, name = main}
Timer fired on <_NSMainThread: 0x2829e4140>{number = 1, name = main}
Stopping timer on <_NSMainThread: 0x2829e4140>{number = 1, name = main}
Starting timer on <NSThread: 0x2829ecf00>{number = 10, name = (null)}

Run 2

Starting timer on <_NSMainThread: 0x281cb8400>{number = 1, name = main}
Timer fired on <_NSMainThread: 0x281cb8400>{number = 1, name = main}
Timer fired on <_NSMainThread: 0x281cb8400>{number = 1, name = main}
Stopping timer on <_NSMainThread: 0x281cb8400>{number = 1, name = main}
Starting timer on <NSThread: 0x281cbc140>{number = 8, name = (null)}

疑问:为什么二次启动后Timer不触发?我猜测和线程有关——启停可能不在同一线程,而invalidate()的文档要求必须在Timer启动的线程调用。另外,如何保证启停在同一线程(无需主线程)?


原因分析

你的猜测完全正确:

  1. Timer依赖线程RunLoop:用Timer.scheduledTimer创建的Timer会自动加入当前线程的RunLoop,但如果是临时后台线程,任务执行完后线程会退出,RunLoop也会停止,Timer失去驱动自然无法触发。
  2. invalidate()的线程限制:苹果文档明确要求,必须在Timer所属的RunLoop线程调用invalidate(),跨线程调用可能导致Timer无法被正确清理;后续在无活跃RunLoop的后台线程创建Timer,也无法正常触发。

从日志能看到,第二次启动Timer的是无名称的后台线程,这类线程通常是一次性的,RunLoop不会持续运行,所以Timer创建后不会触发。


解决方案

方式一:绑定固定线程的RunLoop

创建专门的后台线程并保持其RunLoop运行,所有Timer的启停操作都切换到该线程执行:

private var timer: Timer? = nil
private let timerQueue = DispatchQueue(label: "com.yourdomain.timerQueue")
private var timerThread: Thread?

private func setupTimerThread() {
    guard timerThread == nil else { return }
    timerThread = Thread { [weak self] in
        guard let self = self else { return }
        // 添加空端口防止RunLoop启动后立即退出
        let port = Port()
        RunLoop.current.add(port, forMode: .default)
        while !Thread.current.isCancelled {
            RunLoop.current.run(mode: .default, before: .distantFuture)
        }
    }
    timerThread?.start()
}

private func startTimer() {
    timerQueue.async { [weak self] in
        guard let self = self, self.timer == nil else { return }
        self.setupTimerThread()
        guard let thread = self.timerThread, thread.isExecuting else { return }
        Thread.perform(#selector(self._startTimerOnThread), on: thread, with: nil, waitUntilDone: true)
    }
}

@objc private func _startTimerOnThread() {
    logger.error("Starting timer on \(Thread.current)")
    timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { [weak self] timer in
        self?.logger.error("Timer fired on \(Thread.current)")
    })
}

private func stopTimer() {
    timerQueue.async { [weak self] in
        guard let self = self, let timer = self.timer else { return }
        guard let thread = self.timerThread, thread.isExecuting else {
            self.timer = nil
            return
        }
        Thread.perform(#selector(self._stopTimerOnThread), on: thread, with: nil, waitUntilDone: true)
    }
}

@objc private func _stopTimerOnThread() {
    logger.error("Stopping timer on \(Thread.current)")
    timer?.invalidate()
    timer = nil
}

方式二:使用GCD定时器替代(更简洁)

如果不需要依赖Timer类,GCD的DispatchSourceTimer是更优选择,它不依赖RunLoop,天然支持在指定队列执行,自动保证启停在同一队列:

private var timer: DispatchSourceTimer? = nil
private let timerQueue = DispatchQueue(label: "com.yourdomain.timerQueue")

private func startTimer() {
    guard timer == nil else { return }
    logger.error("Starting timer on \(Thread.current)")
    timer = DispatchSource.makeTimerSource(queue: timerQueue)
    timer?.schedule(deadline: .now(), repeating: 1)
    timer?.setEventHandler { [weak self] in
        self?.logger.error("Timer fired on \(Thread.current)")
    }
    timer?.resume()
}

private func stopTimer() {
    guard let timer = timer else { return }
    logger.error("Stopping timer on \(Thread.current)")
    timer.cancel()
    self.timer = nil
}

内容的提问来源于stack exchange,提问作者denver

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 01:59:56