AVAudioEngine连接inputNode与mainMixerNode时崩溃问题排查
我用以下代码将AVAudioEngine的inputNode连接到mainMixerNode:
public func setupAudioEngine() { self.engine = AVAudioEngine() let format = engine.inputNode.inputFormat(forBus: 0) //main mixer node is connected to output node by default engine.connect(self.engine.inputNode, to: self.engine.mainMixerNode, format: format) do { engine.prepare() try self.engine.start() } catch { print("error couldn't start engine") } engineRunning = true }
但在Crashlytics控制台中出现了无法复现的崩溃,崩溃信息如下:
Fatal Exception: com.apple.coreaudio.avfaudio required condition is false: IsFormatSampleRateAndChannelCountValid(format)
调用setupAudioEngine前,我已经确认AVAudioSession类别不是仅播放类(麦克风不可用的类别),该函数是在处理音频路由变化通知时调用的,并且专门做了这个条件检查。请问哪里操作有误?
完整崩溃堆栈:
Fatal Exception: com.apple.coreaudio.avfaudio 0 CoreFoundation 0x99288 __exceptionPreprocess 1 libobjc.A.dylib 0x16744 objc_exception_throw 2 CoreFoundation 0x17048c -[NSException initWithCoder:] 3 AVFAudio 0x9f64 AVAE_RaiseException(NSString*, ...) 4 AVFAudio 0x55738 AVAudioEngineGraph::_Connect(AVAudioNodeImplBase*, AVAudioNodeImplBase*, unsigned int, unsigned int, AVAudioFormat*) 5 AVFAudio 0x5cce0 AVAudioEngineGraph::Connect(AVAudioNode*, AVAudioNode*, unsigned long, unsigned long, AVAudioFormat*) 6 AVFAudio 0xdf1a8 AVAudioEngineImpl::Connect(AVAudioNode*, AVAudioNode*, unsigned long, unsigned long, AVAudioFormat*) 7 AVFAudio 0xe0fc8 -[AVAudioEngine connect:to:format:] 8 MyApp 0xa6af8 setupAudioEngine + 701 (MicrophoneOutput.swift:701) 9 MyApp 0xa46f0 handleRouteChange + 378 (MicrophoneOutput.swift:378) 10 MyApp 0xa4f50 @objc MicrophoneOutput.handleRouteChange(note:) 11 CoreFoundation 0x2a834 __CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__ 12 CoreFoundation 0xc6fd4 ___CFXRegistrationPost_block_invoke 13 CoreFoundation 0x9a1d0 _CFXRegistrationPost 14 CoreFoundation 0x408ac _CFXNotificationPost 15 Foundation 0x1b754 -[NSNotificationCenter postNotificationName:object:userInfo:] 16 AudioSession 0x56f0 (anonymous namespace)::HandleRouteChange(AVAudioSession*, NSDictionary*) 17 AudioSession 0x5cbc invocation function for block in avfaudio::AVAudioSessionPropertyListener(void*, unsigned int, unsigned int, void const*) 18 libdispatch.dylib 0x1e6c _dispatch_call_block_and_release 19 libdispatch.dylib 0x3a30 _dispatch_client_callout 20 libdispatch.dylib 0x11f48 _dispatch_main_queue_drain 21 libdispatch.dylib 0x11b98 _dispatch_main_queue_callback_4CF 22 CoreFoundation 0x51800 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ 23 CoreFoundation 0xb704 __CFRunLoopRun 24 CoreFoundation 0x1ebc8 CFRunLoopRunSpecific 25 GraphicsServices 0x1374 GSEventRunModal 26 UIKitCore 0x514648 -[UIApplication _run] 27 UIKitCore 0x295d90 UIApplicationMain 28 libswiftUIKit.dylib 0x30ecc UIApplicationMain(_:_:_:_:) 29 MyApp 0xc358 main (WhiteBalanceUI.swift) 30 ??? 0x104b1dce4 (Missing)
核心原因
崩溃提示IsFormatSampleRateAndChannelCountValid(format)不成立,说明获取的format无效——要么采样率非法,要么声道数异常。即便检查了AVAudioSession类别,在音频路由变化的瞬间,inputNode的格式可能还未完成初始化,或者路由切换后麦克风设备暂时不可用,导致inputFormat(forBus:0)返回无效格式。
另外,每次调用setupAudioEngine都会重新创建AVAudioEngine实例,但旧实例如果未正确停止/断开,会和新实例争夺音频资源,间接引发inputNode格式异常。
修复步骤
添加格式有效性检查
在使用format前先验证合法性,避免传入无效格式导致崩溃:let format = engine.inputNode.inputFormat(forBus: 0) guard format.sampleRate > 0, format.channelCount > 0 else { print("Invalid input format, aborting setup") return } engine.connect(self.engine.inputNode, to: self.engine.mainMixerNode, format: format)正确清理旧AVAudioEngine实例
重新创建引擎前,先停止并重置旧实例,释放音频资源:public func setupAudioEngine() { // 清理旧引擎 if let existingEngine = self.engine { existingEngine.stop() existingEngine.disconnectNodeOutput(existingEngine.inputNode) existingEngine.reset() } self.engine = AVAudioEngine() // 后续格式检查和连接逻辑... }延迟执行setup逻辑(可选)
路由变化通知触发时系统音频状态可能不稳定,延迟几十毫秒再执行setup,可避免瞬间设备不可用问题:func handleRouteChange(note: NSNotification) { // 检查会话类别后... DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in self?.setupAudioEngine() } }捕获AVAudioEngine连接时的异常
将connect方法放入do-catch块,避免未捕获异常:do { let format = engine.inputNode.inputFormat(forBus: 0) guard format.sampleRate > 0, format.channelCount > 0 else { throw NSError(domain: "AudioSetup", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid input format"]) } engine.connect(self.engine.inputNode, to: self.engine.mainMixerNode, format: format) engine.prepare() try self.engine.start() engineRunning = true } catch { print("Audio engine setup failed: \(error.localizedDescription)") engineRunning = false }
补充说明
音频路由变化场景复杂(如插拔耳机、切换蓝牙设备),系统需要短暂时间完成设备切换。此时直接访问inputNode格式可能得到临时无效值,通过格式校验、资源清理和延迟执行,可覆盖绝大多数边缘情况。
内容的提问来源于stack exchange,提问作者Deepak Sharma

