使用Timer更新播放进度时UI卡顿问题求助
解决播放进度标签卡顿的问题
兄弟,我太懂这种卡顿的糟心了!之前帮好几个开发者解决过类似的问题,你遇到的情况大概率是定时器的运行模式不对,或者在播放器状态变化时没有及时同步定时器的生命周期导致的。我给你拆解下问题根源和靠谱的解决方案:
为什么Timer/GCD会出现卡顿?
- 用
Timer.scheduledTimer默认是挂在RunLoop.main的default模式下的,当主线程在处理UI操作(比如切换歌曲时的界面刷新、滑动列表),default模式会被暂时阻塞,Timer就会延迟触发——这就是为什么标签会停几秒,等主线程忙完才一次性跳正确时间。 - 换成GCD的
DispatchQueue.main.asyncAfter循环的话,本质问题没解决:如果主线程繁忙,异步任务同样会被推迟执行,卡顿现象还是会出现。
最有效的几个解决方案:
方案1:给Timer换个RunLoop模式
把Timer加到RunLoop.main的commonModes里,这样就算主线程在处理UI交互,Timer也能正常按时触发:
var progressTimer: Timer? func startProgressTracking() { // 先停掉旧的定时器,避免重复运行 stopProgressTracking() progressTimer = Timer(timeInterval: 1.0, target: self, selector: #selector(updateProgressLabel), userInfo: nil, repeats: true) if let timer = progressTimer { RunLoop.main.add(timer, forMode: .commonModes) } } func stopProgressTracking() { progressTimer?.invalidate() progressTimer = nil } @objc private func updateProgressLabel() { let currentTime = player.currentPlaybackTime progressLabel.text = String(format: "%.1f", currentTime ?? 0) }
重要提醒:切换歌曲、暂停/播放时,一定要先调用stopProgressTracking()再重新启动定时器,确保和播放器状态完全同步。
方案2:直接监听AVPlayer的currentTime(最推荐)
比起每秒轮询,让播放器主动通知你时间变化才是最精准高效的方式,完全不会有延迟问题。用KVO就能实现:
var timeObservation: NSKeyValueObservation? func setupPlayerTimeObserver() { // 先移除旧的监听 timeObservation?.invalidate() timeObservation = player.observe(\.currentTime, options: [.new]) { [weak self] _, change in guard let self = self, let newTime = change.newValue else { return } let currentSeconds = CMTimeGetSeconds(newTime) self.progressLabel.text = String(format: "%.1f", currentSeconds) } } // 在页面销毁、切换播放器时记得移除监听 func removePlayerTimeObserver() { timeObservation?.invalidate() timeObservation = nil }
这种方式是播放器内部时间变化时立刻通知你,比轮询靠谱太多,几乎不会有任何卡顿或延迟。
方案3:用CADisplayLink(适合带进度条的场景)
如果你的界面除了标签还有进度条,用CADisplayLink会更合适——它和屏幕刷新率同步,进度显示会更平滑:
var displayLink: CADisplayLink? func startSmoothProgressTracking() { stopSmoothProgressTracking() displayLink = CADisplayLink(target: self, selector: #selector(updateProgress)) displayLink?.add(to: .main, forMode: .commonModes) } func stopSmoothProgressTracking() { displayLink?.invalidate() displayLink = nil } @objc private func updateProgress() { let currentSeconds = CMTimeGetSeconds(player.currentTime()) progressLabel.text = String(format: "%.1f", currentSeconds) // 如果有进度条的话: // progressBar.progress = Float(currentSeconds / CMTimeGetSeconds(player.duration)) }
核心注意事项:
- 无论用哪种方案,在播放器状态变化时(切换歌曲、暂停、播放、停止)一定要及时销毁旧的定时器/监听,再创建新的,否则会出现多个定时器同时运行,或者监听旧播放器的情况,导致进度显示混乱。
- 所有闭包或Timer的target里一定要用
weak self,避免循环引用导致内存泄漏。
内容的提问来源于stack exchange,提问作者Janosch Hübner
相关产品推荐
相关产品推荐

