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启动的线程调用。另外,如何保证启停在同一线程(无需主线程)?
原因分析
你的猜测完全正确:
- Timer依赖线程RunLoop:用
Timer.scheduledTimer创建的Timer会自动加入当前线程的RunLoop,但如果是临时后台线程,任务执行完后线程会退出,RunLoop也会停止,Timer失去驱动自然无法触发。 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
相关产品推荐
相关产品推荐

