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

Swift中AVAudioPlayer播放音频时Timer动画卡顿问题解决

问题分析与解决方案: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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.29 09:00:12