Swift中AVAudioPlayer播放音频时Timer动画卡顿问题解决
问题场景回顾
你遇到的这个情况我之前做动画时也碰到过:点击树叶触发touchedLeaves()播放tap.mp3时,音频正常播放,但Timer驱动的树叶下落动画却会瞬间卡顿。结合你贴的代码,我来拆解下背后的原因和对应的修复方案。
卡顿原因分析
RunLoop模式不匹配导致Timer暂停
默认情况下,Timer.scheduledTimer()会把Timer添加到当前RunLoop的.default模式中。当用户触发UI交互(比如点击屏幕)时,RunLoop会切换到.tracking模式来处理触摸事件,这时候.default模式下的Timer会暂时停止执行,直到交互结束才恢复,所以你会看到动画瞬间卡顿。音效播放的同步IO阻塞主线程
你的SoundManager.playSE()方法每次播放音效时,都会同步从Bundle读取音频文件并初始化AVAudioPlayer。这个文件IO操作是在主线程执行的,会短暂阻塞主线程的动画更新,进一步加剧卡顿现象。
解决方案
1. 调整Timer的RunLoop模式,确保动画不受UI交互影响
把Timer添加到RunLoop的.common模式中,这个模式会在.default和.tracking等模式下都执行,这样UI交互时Timer不会暂停。修改startFalling()方法:
func startFalling(){ leavesView = UIImageView(frame: CGRect(x: 0, y: 0, width: 30, height: 70)) leavesView.image = UIImage(named:"leaves_image")! self.addSubview(leavesView) fallingLeavesTimer = Timer(timeInterval: TimeInterval(0.01), target: self, selector: #selector(updatePositionY(timer:)), userInfo: nil, repeats: true) // 将Timer添加到RunLoop的common模式,避免UI交互时暂停 RunLoop.current.add(fallingLeavesTimer!, forMode: .common) }
2. 预加载音效资源,避免重复初始化AVAudioPlayer
修改SoundManager,提前预加载需要的音效文件,或者缓存已经初始化的AVAudioPlayer,避免每次播放都做同步IO操作:
import Foundation import AVFoundation class SoundManager { // 缓存已加载的音频播放器,避免重复初始化 private var sePlayers: [String: AVAudioPlayer] = [:] init() { // 提前预加载常用音效,比如这里预加载tap.mp3 preloadSE(se: "tap") } // 预加载音效方法,提前完成文件读取和初始化 private func preloadSE(se: String) { guard let audioFileURL = Bundle.main.url(forResource: se, withExtension: "mp3") else { print("找不到音效文件:\(se)") return } do { let player = try AVAudioPlayer(contentsOf: audioFileURL) player.prepareToPlay() // 提前准备播放,减少播放延迟 sePlayers[se] = player } catch let error { print("加载音效失败:\(error.localizedDescription)") } } func playSE(se: String) { guard let player = sePlayers[se] else { print("音效未预加载,尝试临时加载:\(se)") // 如果没预加载,临时加载一次并存入缓存 preloadSE(se: se) playSE(se: se) return } // 支持重复点击播放,播放前重置状态 if player.isPlaying { player.stop() player.currentTime = 0 } player.play() } }
额外优化建议:用CADisplayLink替代Timer做动画
Timer的时间间隔是基于系统时钟的,而CADisplayLink是和屏幕刷新率同步的(通常60fps,约0.0167秒一次),用它来驱动动画会更流畅,也能避免Timer的精度问题:
private var displayLink: CADisplayLink? func startFalling(){ leavesView = UIImageView(frame: CGRect(x: 0, y: 0, width: 30, height: 70)) leavesView.image = UIImage(named:"leaves_image")! self.addSubview(leavesView) displayLink = CADisplayLink(target: self, selector: #selector(updatePositionY)) displayLink?.add(to: .current, forMode: .common) } @objc func updatePositionY(){ leavesView.frame.origin.y += 1 } // 记得在合适的地方(比如viewWillDisappear)停止displayLink,避免内存泄漏 func stopFalling() { displayLink?.invalidate() displayLink = nil }
这样修改后,动画的流畅度会更好,也不会再因为UI交互或者音效播放而卡顿。
内容的提问来源于stack exchange,提问作者K.K.D

