Timer类型以及使用注意事项
1. NSTimer
NSTimer
是一个基于运行循环(RunLoop)的计时器。它可以在一个指定的时间间隔内重复触发一个方法。你可以使用 scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
或 timerWithTimeInterval:target:selector:userInfo:repeats:
方法创建一个 NSTimer 实例。注意,NSTimer 对象对其目标对象(target)有一个强引用(即使target是weak的),这可能导致循环引用的问题,另外还要注意计时器本身不释放的问题。要避免这些情况,一方面避免Timer对象对其目标对象(target)的强引用以及在适当的时候手动使计时器失效(invalidate)。
为了解决这个问题,您可以采用以下策略:
1.1 手动断开引用
在一些情况下,您可能需要在特定的时间点手动断开对象之间的引用。例如,您可能希望在任务完成时停止计时器并将其设置为 nil
,从而断开对当前对象的引用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class NSTimerDemo { var timer: Timer?
func startTimer() { timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(timerFired), userInfo: nil, repeats: true) }
@objc func timerFired() { print("Timer fired!") if someCondition { timer?.invalidate() timer = nil } } }
|
请注意,这种方法需要您在适当的时候手动管理引用,否则可能会导致内存泄漏。而且,不要忘记在适当的时候调用 invalidate()
来停止计时器并释放资源。
1.2 使用 WeakProxy
WeakProxy
可以帮助解决循环引用问题,但它不能完全替代 invalidate()
。WeakProxy
是一种代理模式,它的主要目的是在不创建循环引用的情况下,将消息转发给实际目标对象。在使用 NSTimer
或 Timer
时,可以使用 WeakProxy
来避免循环引用,但即使使用了 WeakProxy
,当不需要计时器时,你仍然需要调用 invalidate()
来停止计时器并释放资源。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| class WeakProxy: NSObject { weak var target: NSObjectProtocol?
init(target: NSObjectProtocol) { self.target = target super.init() }
override func responds(to aSelector: Selector!) -> Bool { return (target?.responds(to: aSelector) ?? false) || super.responds(to: aSelector) }
override func forwardingTarget(for aSelector: Selector!) -> Any? { return target } }
class NSTimerProxyDemo: NSObject { var timer: Timer?
func startTimer() { let proxy = WeakProxy(target: self) timer = Timer.scheduledTimer(timeInterval: 1.0, target: proxy, selector: #selector(timerFired), userInfo: nil, repeats: true) }
@objc func timerFired() { print("Timer fired!") }
deinit { timer?.invalidate() } }
|
在这个示例中,我们创建了一个 WeakProxy
类,它可以将消息转发给实际的目标对象(在本例中是 NSTimerProxyDemo
的实例)。然后,在创建计时器时,我们使用 WeakProxy
实例作为其目标。这样可以避免计时器对目标对象的强引用,从而避免循环引用。
1.3 使用 block-based API
来创建一个带有 block 的计时器:
使用 [weak self]
来捕获一个对当前对象的弱引用,从而避免循环引用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| class BlockTimer { var timer: Timer?
func startTimer() { timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in self?.timerFired() } }
func timerFired() { print("Timer fired!") }
deinit { timer?.invalidate() } }
|
1.4 给NSTimer写个扩展,target指向类对象本身(相当于自己实现block-based API
):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class BlockSupportTimer: NSObject { typealias TimerBlock = () -> Void @objc private class func qb_blockTimerInvoke(_ timer: Timer) { if let block = timer.userInfo as? TimerBlock { block() } } class func qb_scheduledTimer(withTimeInterval interval: TimeInterval, block: @escaping TimerBlock, repeats: Bool) -> Timer { return Timer.scheduledTimer(timeInterval: interval, target: self, selector: #selector(qb_blockTimerInvoke(_:)), userInfo: block, repeats: repeats) } }
|
这段代码中target是类对象,类对象本身不会受到引用计数(retain/release)机制的影响。其生命周期是由 Objective-C 运行时系统管理的,它们在应用程序启动时创建,一直存在于应用程序的整个生命周期中,直到应用程序结束,因此不需要担心引用计数问题。
1.5 子线程中使用 NSTimer
需要将计时器添加到子线程的 RunLoop 中,以确保子线程保持活动状态,从而使计时器能够正常工作。以下是在子线程中使用 NSTimer
并保持子线程活动的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| class TimerViewController: UIViewController {
private var timerThread: Thread?
override func viewDidLoad() { super.viewDidLoad() startTimerInChildThread() }
private func startTimerInChildThread() { timerThread = Thread(target: self, selector: #selector(startTimerInThread), object: nil) timerThread?.start() }
@objc private func startTimerInThread() { autoreleasepool { let timer = Timer(timeInterval: 1, target: self, selector: #selector(timerFired), userInfo: nil, repeats: true) RunLoop.current.add(timer, forMode: .common) RunLoop.current.run() } }
@objc private func timerFired() { print("Timer fired in child thread") }
private func stopTimerAndChildThread() { timerThread?.cancel() timerThread = nil }
deinit { stopTimerAndChildThread() } }
|
使用这种方法,子线程的 RunLoop 会保持活动状态,从而使 NSTimer
能够在子线程中正常运行。请注意,在适当的时候,您需要手动停止计时器并结束子线程的 RunLoop,以避免资源浪费和潜在的内存泄漏问题。
1.5 用NSTimer实现暂停和恢复功能:
NSTimer
本身没有直接的暂停和恢复功能。但是,可以通过一种间接的方式实现暂停和重新开始计时器的效果。以下是一种实现暂停和恢复 NSTimer
的方法,但是建议还是用GCD的Timer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| class PausableTimerWrapper { var timer: Timer? var startTime: Date? var timeInterval: TimeInterval? var block: (() -> Void)?
init(timeInterval: TimeInterval, block: @escaping () -> Void) { self.timeInterval = timeInterval self.block = block self.startTimer() }
@objc func timerFired() { block?() }
func startTimer() { if timer == nil { startTime = Date() timer = Timer.scheduledTimer(timeInterval: timeInterval!, target: self, selector: #selector(timerFired), userInfo: nil, repeats: true) } }
func pauseTimer() { timer?.invalidate() timer = nil }
func resumeTimer() { if let startTime = startTime, let block = block, let timeInterval = timeInterval { if timer == nil { timer = Timer.scheduledTimer(timeInterval: timeInterval, target: self, selector: #selector(timerFired), userInfo: nil, repeats: true) } } }
deinit { timer?.invalidate() timer = nil } }
|
这种方法并不是真正地暂停和恢复同一个计时器实例,而是通过创建新的计时器实例来实现暂停和恢复的效果。在使用此方法时,请确保正确处理计时器的释放和内存管理。
2. CADisplayLink
这是一个用于同步屏幕刷新率的计时器,适用于需要进行高精度动画的场景。它可以确保在屏幕每次刷新时都会调用一个指定的方法。使用 CADisplayLink(target:selector:)
方法创建一个 CADisplayLink 实例,并将其添加到运行循环中。
3. DispatchSourceTimer
这是一个基于 GCD(Grand Central Dispatch)的计时器。你可以使用 DispatchSource.makeTimerSource(queue:)
方法创建一个 DispatchSourceTimer 实例,并使用 schedule(deadline:repeating:leeway:)
方法设置计时器的触发时间和重复间隔。这种计时器可以在多个线程之间安全地使用,无需担心循环引用问题。
和NSTimer的不同:
线程模型:NSTimer
与一个具体的 RunLoop 关联,运行在创建计时器的线程中。这意味着如果 RunLoop 被阻塞,计时器将不会触发。而 DispatchSourceTimer
不受线程限制,它使用 Grand Central Dispatch(GCD)在指定队列上同步(串行队列)或者(并行队列)异步执行。
精度:DispatchSourceTimer
提供了更高的精度,因为它不受 RunLoop 的影响。相比之下,NSTimer
的精度可能受到 RunLoop 阻塞的影响。
暂停和恢复:DispatchSourceTimer
提供了方便的 suspend()
和 resume()
方法用于暂停和恢复计时器。而对于 NSTimer
,您需要手动使计时器失效(invalidate()
)并在需要时重新创建。
内存管理:NSTimer
对其目标对象(通常是 self)有一个强引用(非block的api),可能导致循环引用,因此需要额外关注内存管理。而 DispatchSourceTimer
可以避免此问题,因为它不直接引用目标对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| class GCDTimerDemo { private var timer: DispatchSourceTimer?
func startTimer() { guard timer == nil else { return }
timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global()) timer?.schedule(deadline: .now(), repeating: .seconds(1)) timer?.setEventHandler { [weak self] in print("Timer fired!") DispatchQueue.main.async { } } timer?.resume() }
func pauseTimer() { timer?.suspend() }
func resumeTimer() { timer?.resume() }
func cancelTimer() { timer?.cancel() timer = nil }
deinit { cancelTimer() } }
|
使用RxSwift实现 timer 的功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
private func startNextLectureTimer() { guard let nextLectureInfo = courseInfoModel?.nextLectureInfo, nextLectureInfo.isHasNextLecture == true else { return } let timer = Observable<Int>.interval(.seconds(1), scheduler: MainScheduler.instance) timer.map { [weak self] timeElapsed -> Int in guard let _ = self else { return 0 } return nextLectureInfo.countDown - timeElapsed } .take(nextLectureInfo.countDown + 1) .map { remainingSeconds -> String in let minutes = remainingSeconds / 60 let seconds = remainingSeconds % 60 return String(format: "下节课 %02d:%02d", minutes, seconds) } .bind(to: nextTimeLabel.rx.text) .disposed(by: disposeBag) }
|
- 创建一个 Observable 计时器
timer
,使用 Observable<Int>.interval(.seconds(1), scheduler: MainScheduler.instance)
。这个计时器每秒发出一个事件,事件值为自计时器启动以来经过的秒数。这里的 scheduler
参数表示计时器运行在主线程上,这对于更新 UI 元素很重要。
- 使用
map
操作符将计时器发出的事件值(经过的秒数)转换为剩余秒数。这是通过将 nextLectureInfo.countDown
(下一堂课的倒计时总时长)减去 timeElapsed
(已经过去的秒数)来实现的。注意 weak self
的使用,以避免循环引用。
- 使用
take
操作符限制计时器发出的事件数量。这里使用 nextLectureInfo.countDown + 1
作为事件数量,确保计时器在倒计时结束时停止。
- 再次使用
map
操作符,将剩余秒数转换为 mm:ss
格式的字符串。这里将剩余秒数转换为分钟和秒数,然后使用 String(format:)
方法将其格式化为 “下节课 %02d:%02d” 的字符串。
- 使用
bind(to:)
操作符将格式化后的字符串绑定到 nextTimeLabel.rx.text
。这意味着每次计时器发出事件时,nextTimeLabel
的文本都会更新为当前显示的倒计时。
ps:可以在源码ConcurrentDispatchQueueScheduler中查看到,RX 内部是基于DispatchSourceTimer 实现的。利用rx的监听模式,可以创建一个shared单例的timer observable挂载多种任务。可以优雅的实现app的定时任务,比如数据上报定时器,心跳检测等;
用DispatchSourceTimer
设计一个页面时长统计器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| class PointManager { private var timer: DispatchSourceTimer? private var timerTask: (()->Void)? private var interval: Int = 5 private var deadline: DispatchTime = .now()
static let shareInstance = PointManager() override private init() { super.init() }
private func setupTimer() { timer = DispatchSource.makeTimerSource(queue: DispatchQueue.main()) timer?.schedule(deadline: .now(), repeating: .seconds(6)) timer?.setEventHandler { [weak self] in print("Timer fired!") DispatchQueue.main.async { self.timerTask() } } timer?.resume() } public func configTimer(interval: Int, deadline: DispatchTime, task: (()->Void)?) { timerTask = task self.interval = interval self.deadline = deadline setupTimer() } public func relayTimer(_ afterSecond: Double) { if let timer = timer { let now = DispatchTime.now() let nextStartTime = now + afterSecond let leeway = DispatchTimeInterval.milliseconds(100) timer.schedule(deadline: nextStartTime, repeating: .seconds(interval), leeway: leeway) } }
public func reported(_ point: String, params: JSON) {
} }
|
外部使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| func configGlobalTask() { let pageAppearTask = { if let curVC = UIViewController.current as? ViewController, let logName = curVC.logName { PointManager.reported("all_page_t", params: ["source": logName]) debugPrint("PointManager === \(logName)") } } PointManager.shared.configTimer(interval: 5, deadline: DispatchTime.now() + 5, task: pageAppearTask) ViewController.gloablAppearTask = { PointManager.shared.relayTimer(1) } }
|