SwiftUI+AudioKit iOS应用从后台打开时卡顿问题求助
我正在开发一款支持macOS和iOS的调音器应用,使用AudioKit处理音频逻辑。macOS端运行正常,但iOS端将应用置于后台后再切回前台时,约有半数概率出现卡顿,后台停留时间越长触发概率越高。
推测卡顿原因是:AudioKit的AudioEngine从后台恢复后未正常启动,导致应用无法获取音频数据,进而停止UI更新。我已尝试三种音频引擎启停方案,均未解决问题:
- 使用
.onAppear/.onDisappear控制引擎启停 - 监听
scenePhase状态切换控制引擎 - 保持引擎持续运行
可复现问题的完整代码
import AudioKit import SoundpipeAudioKit // PitchTap import AudioKitEX // Fader import AVFoundation // AVAudioSession import SwiftUI struct ContentView: View { @StateObject var td = ToneDetector() var body: some View { VStack { Text(td.data.note) .font(.system(size: 100, design: .serif)) } .task { await PermissionsChecker.getMicrophoneAccess() } .task { if !td.engine.avEngine.isRunning { td.start() } } } } struct TunerData { var note = "-" } class ToneDetector : ObservableObject, HasAudioEngine { @Published var data = TunerData() let engine = AudioEngine() let mic: AudioEngine.InputNode let tappableA: Fader let tappableB: Fader let silence: Fader var tracker: PitchTap! let noteFrequencies: [Float] = [16.35, 17.32, 18.35, 19.45, 20.6, 21.83, 23.12, 24.5, 25.96, 27.5, 29.14, 30.87] let noteNamesFlats = ["C", "C♯", "D", "D♯", "E", "F", "F♯", "G", "G♯", "A", "A♯", "B"] let noteNamesSharps = ["C", "D♭", "D", "E♭", "E", "F", "G♭", "G", "A♭", "A", "B♭", "B"] init() { guard let inputTmp = engine.input else { fatalError() } mic = inputTmp tappableA = Fader(mic) tappableB = Fader(tappableA) silence = Fader(tappableB, gain: 0) engine.output = silence PermissionsChecker.setSessionParameters() tracker = PitchTap(mic) { pitch, amp in DispatchQueue.main.async { self.update(pitch[0], amp[0]) } } tracker.start() } func update(_ pitch: AUValue, _ amp: AUValue) { guard amp > 0.07 else { return } // Get the frequency down or up to the range that we know var frequency = pitch while frequency > Float(noteFrequencies[noteFrequencies.count - 1]) { frequency /= 2.0 } while frequency < Float(noteFrequencies[0]) { frequency *= 2.0 } // Find the known note frequency we are CLOSEST to var minDistance: Float = 10000.0 var index = 0 for possibleIndex in 0..<noteFrequencies.count { let distance = fabsf(Float(noteFrequencies[possibleIndex]) - frequency) if distance < minDistance { index = possibleIndex minDistance = distance } } if noteNamesFlats[index] == noteNamesSharps[index] { data.note = "\(noteNamesFlats[index])" } else { data.note = "\(noteNamesFlats[index]) / \(noteNamesSharps[index])" } } } class PermissionsChecker { static func getMicrophoneAccess() async { if #available(iOS 17.0, *) { let permission = AVAudioApplication.shared.recordPermission switch permission { case .granted: return case .denied: print("Microphone permission not granted.") case .undetermined: break default: break } await AVAudioApplication.requestRecordPermission() } } static func setSessionParameters() { #if os(iOS) do { Settings.bufferLength = .short try AVAudioSession.sharedInstance().setPreferredIOBufferDuration(Settings.bufferLength.duration) try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .default, options: [.defaultToSpeaker, .mixWithOthers, .allowBluetooth]) try AVAudioSession.sharedInstance().setActive(true) } catch { print("Failed to configure AVAudioSession.") } #endif } }
已尝试的三种方案代码
尝试1:使用.onAppear/.onDisappear
struct ContentView: View { let engine = AudioEngine() var body: some View { VStack { ... } .onAppear() { engine.start() } .onDisappear() { engine.stop() } } }
尝试2:监听scenePhase
struct ContentView: View { let engine = AudioEngine() @Environment(\.scenePhase) var scenePhase var body: some View { VStack { ... } .onChange(of: scenePhase) { oldPhase, newPhase in if newPhase == .active { if !engine.avEngine.isRunning { engine.start() } } else if newPhase == .inactive { if engine.avEngine.isRunning { engine.stop() } } else if newPhase == .background { if engine.avEngine.isRunning { engine.stop() } } } } }
尝试3:保持引擎持续运行
struct ContentView: View { let engine = AudioEngine() var body: some View { VStack { ... } .task { if !engine.avEngine.isRunning { engine.start() } } } }
排查方向与解决思路
同步AVAudioSession与AudioEngine状态
iOS后台时AVAudioSession可能被系统暂停,恢复时不要依赖avEngine.isRunning的判断(该值可能与实际session状态不一致)。需先重新激活session,再启动引擎,并处理激活失败的情况:func restartAudioEngine() { #if os(iOS) do { try AVAudioSession.sharedInstance().setActive(true, options: .notifyOthersOnDeactivation) if !engine.isRunning { try engine.start() tracker?.start() // 重启PitchTap监听 } } catch { print("重启音频引擎失败:\(error)") } #endif }重启PitchTap监听
后台状态下PitchTap可能被终止,仅重启引擎无法恢复音频数据监听。需在应用恢复前台时,手动重启PitchTap。调整引擎启停策略
避免在background/inactive状态下手动停止引擎,让系统自动暂停音频会话;恢复前台时,通过系统通知触发音频链路的重新初始化,减少手动启停带来的状态不一致问题。优化异步启动时序
当前的.task启动逻辑可能与权限请求、session设置存在时序冲突。需确保麦克风权限获取完成后,再启动引擎:.task { await PermissionsChecker.getMicrophoneAccess() if !td.engine.isRunning { do { try td.start() } catch { print("启动引擎失败:\(error)") } } }监听音频会话通知
注册AVAudioSession.interruptionNotification和AVAudioSession.routeChangeNotification,在收到中断结束通知时重新初始化音频组件:// 在ToneDetector的init中添加监听 NotificationCenter.default.addObserver(self, selector: #selector(handleAudioInterruption(_:)), name: AVAudioSession.interruptionNotification, object: nil) @objc func handleAudioInterruption(_ notification: Notification) { guard let info = notification.userInfo, let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt, let type = AVAudioSession.InterruptionType(rawValue: typeValue) else { return } if type == .ended { restartAudioEngine() } }检查AudioKit版本
确认当前使用的AudioKit版本是否存在后台恢复的已知bug,尝试升级到最新稳定版。
内容的提问来源于stack exchange,提问作者Sophie_S

