Foundation — Timer

在 Swift 中,Timer 是一个非常常用的类,用于在指定的时间间隔内执行某些代码块。它可以用于实现定时任务、延迟执行、重复任务等功能。

以下是关于 Timer 的详细介绍,包括其使用方式、常见场景以及注意事项。


1. Timer 的基本概念 #

Timer 是 Foundation 框架中的一个类,主要用于在指定时间间隔后触发某个任务。它可以:

  • 一次性触发:只执行一次任务。
  • 重复触发:按照指定时间间隔重复执行任务。

2. Timer 的创建方式 #

2.1 创建一个重复触发的 Timer #

使用 Timer.scheduledTimer 方法创建一个定时器,指定时间间隔、是否重复以及触发的代码块。

let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
    print("Timer fired!")
}

参数说明: #

  • withTimeInterval:时间间隔(单位:秒)。
  • repeats:是否重复触发。
    • true 表示定时器会重复触发。
    • false 表示定时器只触发一次。
  • 闭包:触发时执行的代码。

2.2 创建一个一次性触发的 Timer #

如果只想触发一次,可以将 repeats 设置为 false

let timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false) { _ in
    print("This will only run once after 5 seconds.")
}

2.3 使用 Timer 和目标方法 #

除了使用闭包,还可以通过指定目标方法(selector)的方式触发定时器。

示例: #

class TimerExample {
    var timer: Timer?

    func startTimer() {
        // 创建一个定时器,触发 `timerFired` 方法
        timer = Timer.scheduledTimer(timeInterval: 2.0,
                                     target: self,
                                     selector: #selector(timerFired),
                                     userInfo: nil,
                                     repeats: true)
    }

    @objc func timerFired() {
        print("Timer fired!")
    }

    func stopTimer() {
        timer?.invalidate() // 停止定时器
        timer = nil
    }
}

参数说明: #

  • timeInterval:时间间隔。
  • target:定时器触发时调用的目标对象。
  • selector:触发时调用的方法(需要用 @objc 修饰)。
  • userInfo:可以传递额外的信息。
  • repeats:是否重复触发。

2.4 手动启动 Timer #

如果你想手动控制定时器的启动,可以使用 Timerinit 方法创建一个定时器,并将其添加到运行循环中。

示例: #

class TimerExample {
    var timer: Timer?

    func startTimer() {
        timer = Timer(timeInterval: 1.0, repeats: true) { _ in
            print("Timer fired!")
        }
        // 将定时器添加到当前运行循环
        RunLoop.current.add(timer!, forMode: .default)
    }

    func stopTimer() {
        timer?.invalidate() // 停止定时器
        timer = nil
    }
}

3. 停止 Timer #

3.1 使用 invalidate() 方法 #

当你不再需要定时器时,必须调用 invalidate() 方法来停止它,否则定时器会继续运行,占用资源。

示例: #

timer.invalidate()

3.2 注意点 #

  • 调用 invalidate() 后,定时器会立即停止,并从运行循环中移除。
  • 一旦定时器被 invalidate(),就无法再重新启动,需要重新创建一个新的定时器。

4. Timer 的使用场景 #

4.1 倒计时功能 #

实现一个简单的倒计时功能:

class Countdown {
    var timer: Timer?
    var secondsRemaining = 10

    func startCountdown() {
        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
            if self.secondsRemaining > 0 {
                print("Seconds remaining: \(self.secondsRemaining)")
                self.secondsRemaining -= 1
            } else {
                print("Countdown finished!")
                self.timer?.invalidate()
            }
        }
    }
}

4.2 延迟执行 #

使用 Timer 延迟执行某段代码:

Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { _ in
    print("This message is delayed by 3 seconds.")
}

4.3 动画或 UI 刷新 #

定时器可以用于定期刷新 UI,例如更新进度条:

class ProgressBarExample {
    var timer: Timer?
    var progress: Float = 0.0

    func startProgressBar() {
        timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
            self.progress += 0.01
            print("Progress: \(self.progress)")
            if self.progress >= 1.0 {
                print("Progress complete!")
                self.timer?.invalidate()
            }
        }
    }
}

5. 注意事项 #

5.1 Timer 必须与 RunLoop 一起工作 #

Timer 需要运行在一个 RunLoop 中,才能正常触发。Timer.scheduledTimer 会自动将定时器添加到当前线程的运行循环中。

如果你手动创建 Timer(如通过 Timer(timeInterval:)),需要显式地将它添加到运行循环中:

RunLoop.current.add(timer, forMode: .default)

5.2 Timer 的线程问题 #

  • Timer 默认运行在主线程。如果你需要在后台线程中运行 Timer,需要手动创建线程并管理运行循环。

5.3 弱引用 Timer 避免循环引用 #

Timer 会对目标对象(如 self)进行强引用,可能导致循环引用,尤其是在闭包中使用 self 时。

解决方法:使用 [weak self] #

timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
    guard let self = self else { return }
    print("Timer fired!")
}

6. 总结 #

功能方法示例代码
创建重复定时器Timer.scheduledTimer(withTimeInterval:repeats:block:)Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in print("Timer fired!") }
创建一次性定时器Timer.scheduledTimer(withTimeInterval:repeats:block:)Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false) { _ in print("One-time timer fired!") }
停止定时器invalidate()timer.invalidate()
使用目标方法创建定时器Timer.scheduledTimer(timeInterval:target:selector:userInfo:repeats:)Timer.scheduledTimer(timeInterval: 2.0, target: self, selector: #selector(timerFired), userInfo: nil, repeats: true)
手动启动定时器Timer(timeInterval:repeats:block:) + RunLoop.addRunLoop.current.add(timer, forMode: .default)

Timer 与 Tolerance 使用 #

Timertolerance 是什么? #

在 Swift 中,toleranceTimer 的一个属性,用于指定定时器触发时间的容忍度(即允许的触发时间误差范围)。通过设置 tolerance,系统可以稍微调整定时器的触发时间,以优化电量消耗和性能。

为什么需要 tolerance #

  • 优化性能:如果多个定时器的触发时间接近,系统可以将它们的触发时间稍微对齐,从而减少 CPU 唤醒的频率,优化电量消耗。
  • 适合非精确任务:对于不需要精确触发的任务(如更新 UI 或定时检查后台任务),允许一定的时间误差可以显著提高系统效率。

如何使用 tolerance #

Timertolerance 是一个以秒为单位的时间间隔(TimeInterval 类型)。你可以在创建定时器后设置其 tolerance 属性。

示例:设置 tolerance #

let timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { _ in
    print("Timer fired at \(Date())")
}

// 设置容忍度为 1 秒
timer.tolerance = 1.0

解释: #

  • withTimeInterval: 5.0 表示定时器每隔 5 秒触发一次。
  • tolerance = 1.0 表示定时器的触发时间可以在 4 秒到 6 秒之间的某个时间点触发。

完整示例:对比有无 tolerance 的效果 #

以下示例展示了如何使用 tolerance 来优化定时器:

import Foundation

class TimerExample {
    var timer: Timer?

    func startTimer() {
        // 创建一个定时器,每隔 5 秒触发一次
        timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { _ in
            print("Timer fired at \(Date())")
        }

        // 设置容忍度为 1 秒
        timer?.tolerance = 1.0
        print("Timer started with 5-second interval and 1-second tolerance.")
    }

    func stopTimer() {
        timer?.invalidate()
        timer = nil
        print("Timer stopped.")
    }
}

let example = TimerExample()
example.startTimer()

// 停止定时器(在实际应用中,这可能通过用户交互或其他条件触发)
DispatchQueue.main.asyncAfter(deadline: .now() + 20) {
    example.stopTimer()
}

运行结果: #

输出的时间可能不会严格每隔 5 秒触发,而是在 4 秒到 6 秒的范围内触发。例如:

Timer started with 5-second interval and 1-second tolerance.
Timer fired at 2025-01-01 10:00:05 +0000
Timer fired at 2025-01-01 10:00:10 +0000
Timer fired at 2025-01-01 10:00:15 +0000

使用场景 #

1. 非精确任务 #

对于不需要精确触发的任务(如动画更新、后台数据同步等),设置 tolerance 可以显著提高性能。例如:

  • 更新 UI 的刷新频率。
  • 定时检查后台任务的状态。

2. 节省电量 #

在需要优化电量消耗的情况下,设置 tolerance 可以减少 CPU 唤醒的频率。例如:

  • 定时器触发的时间间隔较短(如 1 秒以下)。
  • 应用运行在后台时。

注意事项 #

  1. tolerance 的默认值

    • 如果不设置 tolerance,系统会默认将其设置为 0,表示不允许任何触发时间的误差。
  2. tolerance 的值

    • tolerance 不应超过定时器的时间间隔(timeInterval),否则可能导致定时器触发时间的不确定性。
    • 示例:如果定时器的 timeInterval 是 5 秒,tolerance 应小于或等于 5 秒。
  3. 实时性任务

    • 对于需要严格控制触发时间的任务(如计时器、游戏逻辑、音频播放等),不要设置 tolerance,因为时间误差可能导致不准确的行为。

总结 #

tolerance 的作用: #

  • 允许系统在指定时间间隔内稍微调整定时器的触发时间。
  • 适用于非实时性任务,帮助优化性能和节省电量。

如何使用: #

  1. 创建定时器后,设置 tolerance 属性。
  2. 确保 tolerance 小于等于定时器的时间间隔。

示例代码: #

let timer = Timer.scheduledTimer(withTimeInterval: 10.0, repeats: true) { _ in
    print("Timer fired at \(Date())")
}
timer.tolerance = 2.0 // 设置容忍度为 2 秒

通过合理使用 tolerance,你可以在性能和准确性之间找到平衡。

何时执行 #

什么时候触发 timer #

触发机制: #

  • Timer.scheduledTimer 会自动将定时器添加到当前线程的 运行循环(RunLoop) 中,并开始运行。
  • 定时器会在 10 秒后触发第一次,然后每隔 10 秒触发一次(如果设置了 repeats: true)。
  • 由于设置了 tolerance = 2.0,系统可能会在 8 秒到 12 秒之间的某个时间点触发,而不是严格的 10 秒。

定时器的运行依赖于 RunLoop #

为什么会自动触发? #

  • Timer.scheduledTimer 会将定时器自动加入到当前线程的 默认运行循环模式(RunLoop.Mode.default 中。
  • 运行循环(RunLoop) 是一个事件处理机制,用于管理线程的事件和任务,例如用户交互、定时器触发、网络事件等。
  • 当定时器被添加到运行循环后,运行循环会在合适的时间点检查并触发定时器的回调。

触发过程的完整逻辑 #

  1. 定时器创建:

    • Timer.scheduledTimer 会将定时器添加到当前线程的运行循环中。
    • 定时器会按照指定的时间间隔(timeInterval)等待触发。
  2. 触发时机:

    • 定时器的触发时间是基于运行循环的检查。
    • 如果设置了 tolerance,系统可以稍微调整触发时间,以优化性能。
  3. 触发回调:

    • 到达触发时间后,运行循环会执行定时器的回调代码块(或目标方法)。

如何验证定时器的触发? #

以下代码可以用来验证定时器的触发时间和 tolerance 的影响:

import Foundation

let timer = Timer.scheduledTimer(withTimeInterval: 10.0, repeats: true) { _ in
    print("Timer fired at \(Date())")
}
timer.tolerance = 2.0 // 设置容忍度为 2 秒

// 保持运行循环活跃(仅在 Playground 中需要)
RunLoop.current.run()

运行结果: #

输出的时间可能会有一定的误差。例如:

Timer fired at 2025-01-01 10:00:10 +0000
Timer fired at 2025-01-01 10:00:20 +0000
Timer fired at 2025-01-01 10:00:30 +0000

注意:运行循环的模式 #

定时器默认运行在 RunLoop.Mode.default 模式下。如果当前线程的运行循环正在处理其他模式的任务(例如滚动视图会切换到 RunLoop.Mode.tracking 模式),定时器可能会被暂时挂起,直到运行循环回到默认模式。

解决方法:使用通用模式 #

如果你希望定时器在所有模式下都能正常运行,可以将定时器添加到 RunLoop.Mode.common 模式中:

let timer = Timer.scheduledTimer(withTimeInterval: 10.0, repeats: true) { _ in
    print("Timer fired at \(Date())")
}
timer.tolerance = 2.0

// 将定时器添加到通用模式
RunLoop.current.add(timer, forMode: .common)

总结:什么时候触发? #

  • 默认情况下:

    • Timer.scheduledTimer 会自动添加到运行循环中,定时器会在指定的时间间隔后触发。
    • 触发的时间可能会因为 tolerance 的设置而稍微提前或延后。
  • 触发时间依赖运行循环:

    • 如果线程的运行循环没有运行(例如线程被阻塞),定时器将无法触发。
  • 验证触发:

    • 你可以通过打印当前时间(Date())来验证定时器的触发时机。

如果你还有其他疑问,欢迎随时提问! 😊

本文共 3391 字,上次修改于 Jan 1, 2025