You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.20 10:35:28