重要前提
安装AI Skills的关键前提是:必须科学上网,且开启TUN模式,这一点至关重要,直接决定安装能否顺利完成,在此郑重提醒三遍:科学上网,科学上网,科学上网。查看完整安装教程 →
axiom-timer-patterns-ref by charleswiltgen/axiom
npx skills add https://github.com/charleswiltgen/axiom --skill axiom-timer-patterns-refiOS 定时器机制的完整 API 参考。关于决策树和崩溃预防,请参阅 axiom-timer-patterns。
// 最常见 — 基于闭包,自动添加到当前 RunLoop
let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.updateProgress()
}
关键细节:添加到 .default RunLoop 模式。滚动期间停止。请参阅下文第一部分 RunLoop 模式表。
// Objective-C 风格 — 保留 TARGET(泄漏风险)
let timer = Timer.scheduledTimer(
timeInterval: 1.0,
target: self, // 定时器保留 self!
selector: #selector(update),
userInfo: nil,
repeats: true
)
危险:此 API 会保留 target。如果 self 也持有定时器,则会产生循环引用。使用带有 的基于闭包的 API 总是更安全。
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
[weak self]// 创建定时器但不添加到 RunLoop
let timer = Timer(timeInterval: 1.0, repeats: true) { [weak self] _ in
self?.updateProgress()
}
// 添加到特定的 RunLoop 模式
RunLoop.current.add(timer, forMode: .common) // 滚动时仍存活
timer.tolerance = 0.1 // 允许 100 毫秒的灵活性,供系统合并处理
设置容差后,系统会将触发时间相近的定时器批量处理。最小推荐值:间隔时间的 10%。减少 CPU 唤醒和能耗。
| 模式 | 常量 | 何时激活 | 定时器触发? |
|---|---|---|---|
| 默认 | .default / RunLoop.Mode.default | 正常用户交互 | 是 |
| 跟踪 | .tracking / RunLoop.Mode.tracking | 滚动/拖拽手势激活 | 仅当添加到 .common 时 |
| 通用 | .common / RunLoop.Mode.common | 伪模式(默认 + 跟踪) | 是(始终) |
timer.invalidate() // 停止定时器,从 RunLoop 中移除
// 定时器在 invalidate 后不可重用 — 创建一个新的
timer = nil // 释放引用
关键细节:invalidate() 必须在创建定时器的同一线程(通常是主线程)上调用。
if timer.isValid {
// 定时器仍处于活动状态
}
在 invalidate() 之后或非重复定时器触发后返回 false。
Timer.publish(every: 1.0, tolerance: 0.1, on: .main, in: .common)
.autoconnect()
.sink { [weak self] _ in
self?.updateProgress()
}
.store(in: &cancellables)
完整的 Combine 定时器细节请参阅第三部分。
// 在特定队列上创建定时器源
let queue = DispatchQueue(label: "com.app.timer")
let timer = DispatchSource.makeTimerSource(flags: [], queue: queue)
flags:通常为空 ([])。使用 .strict 进行精确计时(禁用系统合并,能耗更高)。
// 相对截止时间(单调时钟)
timer.schedule(
deadline: .now() + 1.0, // 首次触发
repeating: .seconds(1), // 间隔
leeway: .milliseconds(100) // 容差(类似 Timer.tolerance)
)
// 挂钟截止时间(设备休眠时仍存活)
timer.schedule(
wallDeadline: .now() + 1.0,
repeating: .seconds(1),
leeway: .milliseconds(100)
)
deadline 与 wallDeadline:deadline 使用单调时钟(设备休眠时暂停)。wallDeadline 使用挂钟时间(休眠期间继续)。大多数情况下使用 deadline。
timer.setEventHandler { [weak self] in
self?.performWork()
}
取消前:将处理程序设置为 nil 以打破循环引用:
timer.setEventHandler(handler: nil)
timer.cancel()
timer.activate() // 启动 — 只能调用一次(空闲 → 运行中)
timer.suspend() // 暂停(运行中 → 已暂停)
timer.resume() // 恢复(已暂停 → 运行中)
timer.cancel() // 永久停止(必须处于非暂停状态)
activate()
空闲 ──────────────► 运行中
│ ▲
suspend() │ │ resume()
▼ │
已暂停
│
resume() + cancel()
│
▼
已取消
关键规则:
activate() 只能调用一次(空闲 → 运行中)cancel() 要求处于非暂停状态(如果已暂停,请先恢复)cancelled 是终止状态 — 不允许进一步操作// 容差值
timer.schedule(deadline: .now(), repeating: 1.0, leeway: .milliseconds(100))
timer.schedule(deadline: .now(), repeating: 1.0, leeway: .seconds(1))
timer.schedule(deadline: .now(), repeating: 1.0, leeway: .never) // 严格 — 高能耗
容差是 Timer.tolerance 在 DispatchSourceTimer 中的等价物。允许系统合并定时器触发以提高能效。
在一个代码块中展示完整的 DispatchSourceTimer 生命周期:
let queue = DispatchQueue(label: "com.app.polling")
let timer = DispatchSource.makeTimerSource(queue: queue)
timer.schedule(deadline: .now() + 1.0, repeating: .seconds(5), leeway: .milliseconds(500))
timer.setEventHandler { [weak self] in
self?.fetchUpdates()
}
timer.activate() // 空闲 → 运行中
// 稍后 — 暂停:
timer.suspend() // 运行中 → 已暂停
// 稍后 — 恢复:
timer.resume() // 已暂停 → 运行中
// 清理 — 如果已暂停,必须在取消前恢复:
timer.setEventHandler(handler: nil) // 打破循环引用
timer.resume() // 确保处于非暂停状态
timer.cancel() // 运行中 → 已取消(终止)
关于防止所有崩溃模式的安全包装器,请参阅 axiom-timer-patterns 第四部分:SafeDispatchTimer。
import Combine
// 创建发布者 — RunLoop 模式在这里也很重要
let publisher = Timer.publish(
every: 1.0, // 间隔
tolerance: 0.1, // 可选容差
on: .main, // RunLoop
in: .common // 模式 — 使用 .common 以在滚动时存活
)
// 当第一个订阅者附加时立即启动
Timer.publish(every: 1.0, on: .main, in: .common)
.autoconnect()
.sink { date in
print("Fired at \(date)")
}
.store(in: &cancellables)
// 手动控制定时器何时启动
let timerPublisher = Timer.publish(every: 1.0, on: .main, in: .common)
let cancellable = timerPublisher
.sink { date in
print("Fired at \(date)")
}
// 稍后启动
let connection = timerPublisher.connect()
// 停止
connection.cancel()
// 通过 AnyCancellable 存储 — 当 Set 被清空或对象释放时取消
private var cancellables = Set<AnyCancellable>()
// 手动取消
cancellables.removeAll() // 取消所有订阅
class TimerViewModel: ObservableObject {
@Published var elapsed: Int = 0
private var cancellables = Set<AnyCancellable>()
func start() {
Timer.publish(every: 1.0, tolerance: 0.1, on: .main, in: .common)
.autoconnect()
.sink { [weak self] _ in
self?.elapsed += 1
}
.store(in: &cancellables)
}
func stop() {
cancellables.removeAll()
}
}
// 单调时钟 — 应用挂起时不会暂停
for await _ in ContinuousClock().timer(interval: .seconds(1)) {
await updateData()
}
// 任务取消时循环退出
// 可挂起时钟 — 应用挂起时暂停
for await _ in SuspendingClock().timer(interval: .seconds(1)) {
await processItem()
}
ContinuousClock 与 SuspendingClock:
ContinuousClock:应用挂起期间时间继续前进。用于绝对计时。SuspendingClock:应用挂起时时间暂停。用于“用户感知”计时。// 任务取消时定时器自动停止
let timerTask = Task {
for await _ in ContinuousClock().timer(interval: .seconds(1)) {
await fetchLatestData()
}
}
// 稍后:取消定时器
timerTask.cancel()
func startPolling() async {
do {
for try await _ in ContinuousClock().timer(interval: .seconds(30)) {
try Task.checkCancellation()
let data = try await api.fetchUpdates()
await MainActor.run { updateUI(with: data) }
}
} catch is CancellationError {
// 干净退出
} catch {
// 处理获取错误
}
}
// 简单延迟 — 不是定时器
try await Task.sleep(for: .seconds(1))
// 基于截止时间
try await Task.sleep(until: .now + .seconds(1), clock: .continuous)
| 需求 | 使用 |
|---|---|
| 操作前的单次延迟 | Task.sleep(for:) |
| 重复操作 | ContinuousClock().timer(interval:) |
| 可取消的延迟 | 在 Task 中使用 Task.sleep(for:) |
| 带退避的重试 | 在循环中使用 Task.sleep(for:) |
func fetchWithRetry(maxAttempts: Int = 3) async throws -> Data {
var delay: Duration = .seconds(1)
for attempt in 1...maxAttempts {
do {
return try await api.fetch()
} catch where attempt < maxAttempts {
try await Task.sleep(for: delay)
delay *= 2 // 指数退避
}
}
throw FetchError.maxRetriesExceeded
}
# 检查定时器是否仍然有效
po timer.isValid
# 查看下一次触发日期
po timer.fireDate
# 查看定时器间隔
po timer.timeInterval
# 强制 RunLoop 迭代(可能触发定时器)
expression -l objc -- (void)[[NSRunLoop mainRunLoop] run]
# 检查调度源
po timer
# 在调度源取消时中断(所有源)
breakpoint set -n dispatch_source_cancel
# 在 EXC_BAD_INSTRUCTION 上中断以捕获定时器崩溃
# (Xcode 会自动为 Swift 运行时错误执行此操作)
# 检查 DispatchSource 是否已取消
expression -l objc -- (long)dispatch_source_testcancel((void*)timer)
# 列出主 RunLoop 上的所有定时器
expression -l objc -- (void)CFRunLoopGetMain()
# 在任何 Timer 触发时中断
breakpoint set -S "scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:"
| API | iOS | macOS | watchOS | tvOS |
|---|---|---|---|---|
| Timer | 2.0+ | 10.0+ | 2.0+ | 9.0+ |
| DispatchSourceTimer | 8.0+ (GCD) | 10.10+ | 2.0+ | 9.0+ |
| Timer.publish (Combine) | 13.0+ | 10.15+ | 6.0+ | 13.0+ |
| AsyncTimerSequence | 16.0+ | 13.0+ | 9.0+ | 16.0+ |
| Task.sleep | 13.0+ | 10.15+ | 6.0+ | 13.0+ |
axiom-timer-patterns — 决策树、崩溃模式、SafeDispatchTimer 包装器axiom-energy — 定时器容差作为能量优化(模式 1)axiom-energy-ref — 包含 WWDC 代码示例的定时器效率 APIaxiom-memory-debugging — 定时器作为模式 1 内存泄漏技能 : axiom-timer-patterns, axiom-energy-ref, axiom-memory-debugging
每周安装量
19
仓库
GitHub 星标数
590
首次出现
10 天前
安全审计
安装于
opencode18
github-copilot18
codex18
kimi-cli18
gemini-cli18
amp18
Complete API reference for iOS timer mechanisms. For decision trees and crash prevention, see axiom-timer-patterns.
// Most common — block-based, auto-added to current RunLoop
let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.updateProgress()
}
Key detail : Added to .default RunLoop mode. Stops during scrolling. See Part 1 RunLoop modes table below.
// Objective-C style — RETAINS TARGET (leak risk)
let timer = Timer.scheduledTimer(
timeInterval: 1.0,
target: self, // Timer retains self!
selector: #selector(update),
userInfo: nil,
repeats: true
)
Danger : This API retains target. If self also holds the timer, you have a retain cycle. The block-based API with [weak self] is always safer.
// Create timer without adding to RunLoop
let timer = Timer(timeInterval: 1.0, repeats: true) { [weak self] _ in
self?.updateProgress()
}
// Add to specific RunLoop mode
RunLoop.current.add(timer, forMode: .common) // Survives scrolling
timer.tolerance = 0.1 // Allow 100ms flexibility for system coalescing
System batches timers with similar fire dates when tolerance is set. Minimum recommended: 10% of interval. Reduces CPU wakes and energy consumption.
| Mode | Constant | When Active | Timer Fires? |
|---|---|---|---|
| Default | .default / RunLoop.Mode.default | Normal user interaction | Yes |
| Tracking | .tracking / RunLoop.Mode.tracking | Scroll/drag gesture active | Only if added to .common |
| Common | .common / |
timer.invalidate() // Stops timer, removes from RunLoop
// Timer is NOT reusable after invalidate — create a new one
timer = nil // Release reference
Key detail : invalidate() must be called from the same thread that created the timer (usually main thread).
if timer.isValid {
// Timer is still active
}
Returns false after invalidate() or after a non-repeating timer fires.
Timer.publish(every: 1.0, tolerance: 0.1, on: .main, in: .common)
.autoconnect()
.sink { [weak self] _ in
self?.updateProgress()
}
.store(in: &cancellables)
See Part 3 for full Combine timer details.
// Create timer source on a specific queue
let queue = DispatchQueue(label: "com.app.timer")
let timer = DispatchSource.makeTimerSource(flags: [], queue: queue)
flags : Usually empty ([]). Use .strict for precise timing (disables system coalescing, higher energy cost).
// Relative deadline (monotonic clock)
timer.schedule(
deadline: .now() + 1.0, // First fire
repeating: .seconds(1), // Interval
leeway: .milliseconds(100) // Tolerance (like Timer.tolerance)
)
// Wall clock deadline (survives device sleep)
timer.schedule(
wallDeadline: .now() + 1.0,
repeating: .seconds(1),
leeway: .milliseconds(100)
)
deadline vs wallDeadline : deadline uses monotonic clock (pauses when device sleeps). wallDeadline uses wall clock (continues across sleep). Use deadline for most cases.
timer.setEventHandler { [weak self] in
self?.performWork()
}
Before cancel : Set handler to nil to break retain cycles:
timer.setEventHandler(handler: nil)
timer.cancel()
timer.activate() // Start — can only call ONCE (idle → running)
timer.suspend() // Pause (running → suspended)
timer.resume() // Unpause (suspended → running)
timer.cancel() // Stop permanently (must NOT be suspended)
activate()
idle ──────────────► running
│ ▲
suspend() │ │ resume()
▼ │
suspended
│
resume() + cancel()
│
▼
cancelled
Critical rules :
activate() can only be called once (idle → running)cancel() requires non-suspended state (resume first if suspended)cancelled is terminal — no further operations allowed// Leeway values
timer.schedule(deadline: .now(), repeating: 1.0, leeway: .milliseconds(100))
timer.schedule(deadline: .now(), repeating: 1.0, leeway: .seconds(1))
timer.schedule(deadline: .now(), repeating: 1.0, leeway: .never) // Strict — high energy
Leeway is the DispatchSourceTimer equivalent of Timer.tolerance. Allows system to coalesce timer firings for energy efficiency.
Complete DispatchSourceTimer lifecycle in one block:
let queue = DispatchQueue(label: "com.app.polling")
let timer = DispatchSource.makeTimerSource(queue: queue)
timer.schedule(deadline: .now() + 1.0, repeating: .seconds(5), leeway: .milliseconds(500))
timer.setEventHandler { [weak self] in
self?.fetchUpdates()
}
timer.activate() // idle → running
// Later — pause:
timer.suspend() // running → suspended
// Later — resume:
timer.resume() // suspended → running
// Cleanup — MUST resume before cancel if suspended:
timer.setEventHandler(handler: nil) // Break retain cycles
timer.resume() // Ensure non-suspended state
timer.cancel() // running → cancelled (terminal)
For a safe wrapper that prevents all crash patterns, see axiom-timer-patterns Part 4: SafeDispatchTimer.
import Combine
// Create publisher — RunLoop mode matters here too
let publisher = Timer.publish(
every: 1.0, // Interval
tolerance: 0.1, // Optional tolerance
on: .main, // RunLoop
in: .common // Mode — use .common to survive scrolling
)
// Starts immediately when first subscriber attaches
Timer.publish(every: 1.0, on: .main, in: .common)
.autoconnect()
.sink { date in
print("Fired at \(date)")
}
.store(in: &cancellables)
// Manual control over when timer starts
let timerPublisher = Timer.publish(every: 1.0, on: .main, in: .common)
let cancellable = timerPublisher
.sink { date in
print("Fired at \(date)")
}
// Start later
let connection = timerPublisher.connect()
// Stop
connection.cancel()
// Via AnyCancellable storage — cancelled when Set is cleared or object deallocs
private var cancellables = Set<AnyCancellable>()
// Manual cancellation
cancellables.removeAll() // Cancels all subscriptions
class TimerViewModel: ObservableObject {
@Published var elapsed: Int = 0
private var cancellables = Set<AnyCancellable>()
func start() {
Timer.publish(every: 1.0, tolerance: 0.1, on: .main, in: .common)
.autoconnect()
.sink { [weak self] _ in
self?.elapsed += 1
}
.store(in: &cancellables)
}
func stop() {
cancellables.removeAll()
}
}
// Monotonic clock — does NOT pause when app suspends
for await _ in ContinuousClock().timer(interval: .seconds(1)) {
await updateData()
}
// Loop exits when task is cancelled
// Suspending clock — pauses when app suspends
for await _ in SuspendingClock().timer(interval: .seconds(1)) {
await processItem()
}
ContinuousClock vs SuspendingClock :
ContinuousClock: Time keeps advancing during app suspension. Use for absolute timing.SuspendingClock: Time pauses when app suspends. Use for "user-perceived" timing.// Timer automatically stops when task is cancelled
let timerTask = Task {
for await _ in ContinuousClock().timer(interval: .seconds(1)) {
await fetchLatestData()
}
}
// Later: cancel the timer
timerTask.cancel()
func startPolling() async {
do {
for try await _ in ContinuousClock().timer(interval: .seconds(30)) {
try Task.checkCancellation()
let data = try await api.fetchUpdates()
await MainActor.run { updateUI(with: data) }
}
} catch is CancellationError {
// Clean exit
} catch {
// Handle fetch error
}
}
// Simple delay — NOT a timer
try await Task.sleep(for: .seconds(1))
// Deadline-based
try await Task.sleep(until: .now + .seconds(1), clock: .continuous)
| Need | Use |
|---|---|
| One-shot delay before action | Task.sleep(for:) |
| Repeating action | ContinuousClock().timer(interval:) |
| Delay with cancellation | Task.sleep(for:) in a Task |
| Retry with backoff | Task.sleep(for:) in a loop |
func fetchWithRetry(maxAttempts: Int = 3) async throws -> Data {
var delay: Duration = .seconds(1)
for attempt in 1...maxAttempts {
do {
return try await api.fetch()
} catch where attempt < maxAttempts {
try await Task.sleep(for: delay)
delay *= 2 // Exponential backoff
}
}
throw FetchError.maxRetriesExceeded
}
# Check if timer is still valid
po timer.isValid
# See next fire date
po timer.fireDate
# See timer interval
po timer.timeInterval
# Force RunLoop iteration (may trigger timer)
expression -l objc -- (void)[[NSRunLoop mainRunLoop] run]
# Inspect dispatch source
po timer
# Break on dispatch source cancel (all sources)
breakpoint set -n dispatch_source_cancel
# Break on EXC_BAD_INSTRUCTION to catch timer crashes
# (Xcode does this automatically for Swift runtime errors)
# Check if a DispatchSource is cancelled
expression -l objc -- (long)dispatch_source_testcancel((void*)timer)
# List all timers on the main RunLoop
expression -l objc -- (void)CFRunLoopGetMain()
# Break when any Timer fires
breakpoint set -S "scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:"
| API | iOS | macOS | watchOS | tvOS |
|---|---|---|---|---|
| Timer | 2.0+ | 10.0+ | 2.0+ | 9.0+ |
| DispatchSourceTimer | 8.0+ (GCD) | 10.10+ | 2.0+ | 9.0+ |
| Timer.publish (Combine) | 13.0+ | 10.15+ | 6.0+ | 13.0+ |
| AsyncTimerSequence | 16.0+ | 13.0+ | 9.0+ | 16.0+ |
| Task.sleep | 13.0+ | 10.15+ | 6.0+ | 13.0+ |
axiom-timer-patterns — Decision trees, crash patterns, SafeDispatchTimer wrapperaxiom-energy — Timer tolerance as energy optimization (Pattern 1)axiom-energy-ref — Timer efficiency APIs with WWDC code examplesaxiom-memory-debugging — Timer as Pattern 1 memory leakSkills : axiom-timer-patterns, axiom-energy-ref, axiom-memory-debugging
Weekly Installs
19
Repository
GitHub Stars
590
First Seen
10 days ago
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode18
github-copilot18
codex18
kimi-cli18
gemini-cli18
amp18
TanStack Query v5 完全指南:React 数据管理、乐观更新、离线支持
2,500 周安装
RunLoop.Mode.common| Pseudo-mode (default + tracking) |
| Yes (always) |