iOS中向已运行RunLoop添加Timer异常及主RunLoop模拟问询
自定义RunLoop与主RunLoop的行为差异及模拟方案
问题场景
编写了如下Swift代码,期望三个Timer均能正常触发,但实际仅timer1运行:
let timer1 = Timer(timeInterval: 1, repeats: true) { _ in print("FOO1") } let timer2 = Timer(timeInterval: 1, repeats: true) { _ in print("FOO2") } let timer3 = Timer(timeInterval: 1, repeats: true) { _ in print("FOO3") } let queue = DispatchQueue(label: "") queue.async { RunLoop.current.add(timer1, forMode: .common) RunLoop.current.run() } queue.async { RunLoop.current.add(timer2, forMode: .common) } queue.async { RunLoop.current.add(timer3, forMode: .common) }
测试发现
- 若将
RunLoop.current.run()移至最后一个异步块,所有Timer均可正常运行; - 若在三个异步块中均添加
RunLoop.current.run(),则仅timer1会运行; - 将模式从
.common改为.default无任何改善。
行为差异的核心原因
串行队列的任务阻塞特性
你创建的DispatchQueue(label: "")是默认的串行队列,队列中的异步任务会按顺序在同一个后台线程执行。第一个异步块中调用的RunLoop.current.run()会启动无限循环,直接阻塞当前线程,导致后续两个异步任务永远无法被调度——timer2和timer3根本没机会被添加到RunLoop中,自然不会触发。主RunLoop的运行机制
主线程的RunLoop由系统初始化并持续维护,它采用「处理事件后休眠、有新事件时唤醒」的循环模式,不会一直占用线程导致其他代码无法执行。主线程的所有任务都在该RunLoop的上下文内执行,因此任何时候在主线程添加Timer,都会被RunLoop的下一次循环检测并触发。RunLoop.run()的本质
手动调用RunLoop.current.run()会启动一个无终止的循环,只有调用RunLoop.stop()才会退出。这个调用会完全阻塞当前线程,线程在此期间无法处理队列中后续的任何任务。
模拟主RunLoop的运行行为
要实现类似主RunLoop「持续运行、可动态添加Timer」的特性,可采用以下两种方案:
方案1:串行队列下确保所有Timer先添加再启动RunLoop
利用串行队列的顺序执行特性,将RunLoop.run()放在最后一个任务中,确保所有Timer都已添加到RunLoop后再启动循环:
let queue = DispatchQueue(label: "") queue.async { RunLoop.current.add(timer1, forMode: .common) } queue.async { RunLoop.current.add(timer2, forMode: .common) } queue.async { RunLoop.current.add(timer3, forMode: .common) RunLoop.current.run() }
方案2:创建专用线程模拟主RunLoop
这种方式支持随时动态添加Timer,完全复刻主RunLoop的行为:
// 初始化Timer let timer1 = Timer(timeInterval: 1, repeats: true) { _ in print("FOO1") } let timer2 = Timer(timeInterval: 1, repeats: true) { _ in print("FOO2") } let timer3 = Timer(timeInterval: 1, repeats: true) { _ in print("FOO3") } // 创建专用线程运行RunLoop let runLoopThread = Thread { let currentRunLoop = RunLoop.current // 添加空定时器源,防止RunLoop无事件时立即退出 let keepAliveSource = DispatchSource.makeTimerSource() keepAliveSource.schedule(deadline: .distantFuture) keepAliveSource.setEventHandler {} keepAliveSource.resume() currentRunLoop.add(keepAliveSource, forMode: .common) // 启动RunLoop,采用休眠唤醒模式 while currentRunLoop.run(mode: .common, before: .distantFuture) {} } runLoopThread.start() // 向目标RunLoop添加Timer(确保在对应线程上下文执行) runLoopThread.perform { RunLoop.current.add(timer1, forMode: .common) RunLoop.current.add(timer2, forMode: .common) RunLoop.current.add(timer3, forMode: .common) } // 后续可随时动态添加新Timer DispatchQueue.global().asyncAfter(deadline: .now() + 3) { runLoopThread.perform { let timer4 = Timer(timeInterval: 1, repeats: true) { _ in print("FOO4") } RunLoop.current.add(timer4, forMode: .common) } }
- 专用线程中添加空的
DispatchSource是为了让RunLoop始终有监听事件,避免启动后立即退出; - 使用
Thread.perform(_:)可以安全地在目标线程的RunLoop上下文中添加Timer,避免线程安全问题; - 该模式下RunLoop会持续运行,支持随时添加新的Timer,和主RunLoop行为完全一致。
内容的提问来源于stack exchange,提问作者OMGPOP
相关产品推荐
相关产品推荐

