iOS音频串行队列

iOS 音频操作串行队列避免崩溃 #

快速点击录音按钮导致 EXC_BAD_ACCESS 崩溃,根本原因是 AudioUnit 不是线程安全的。本文展示如何用串行队列解决。

崩溃分析 #

崩溃现场 #

Thread 56: EXC_BAD_ACCESS (code=1, address=0x74757074754f6657)
AUGraphOpen(augStruct->graph) // 访问已释放的 graph

崩溃原因 #

时间线 ─────────────────────────────────────────►

点击1: Thread A ──┬─ stopSpeech() ──┬─ 释放 graph ──┬─ 内存回收
                  │                 │               │
点击2: Thread B ──┼─────────────────┼─ stopSpeech() ┼─ 访问已释放内存
                  │                 │               │
                  ▼                 ▼               ▼
              [正常]            [危险区]        [💥崩溃]

并发队列问题(导致崩溃) #

错误代码 #

// ❌ 危险:全局并发队列
class Coordinator {
    func handleTouchBegan() {
        // 每次点击创建新线程
        DispatchQueue.global(qos: .userInitiated).async {
            self.viewModel.stopSpeech()  // Thread 1
        }
    }
    
    func handleTouchEnded() {
        // 又创建新线程
        DispatchQueue.global(qos: .userInitiated).async {
            HKAITool.shared.sendFinished()  // Thread 2
        }
    }
}

问题剖析 #

  1. 并发执行 - 多个线程同时操作 AudioUnit
  2. 资源竞争 - graph 被多次释放
  3. 状态不一致 - 音频系统内部状态混乱

串行队列解决方案 #

核心改造 #

// ✅ 安全:专用串行队列
class Coordinator {
    // 1. 创建专用串行队列
    private let audioQueue = DispatchQueue(
        label: "com.app.audioQueue",  // 唯一标识
        qos: .userInitiated           // 高优先级
    )
    
    func handleTouchBegan() {
        // 2. UI 立即响应
        feedbackGenerator.impactOccurred()
        withAnimation(.easeOut(duration: 0.05)) {
            self.chatingBarPressStatus = .LongPressing
        }
        
        // 3. 音频操作放入串行队列
        audioQueue.async { [weak self] in
            guard let self = self else { return }
            
            // 串行执行,绝不会并发
            self.viewModel.stopSpeech()
            
            // 小延迟确保资源释放(可选)
            Thread.sleep(forTimeInterval: 0.01)
            
            // 开始新的录音
            HKAITool.shared.sendSocket { uuid, text in
                // 回到主线程更新 UI
                DispatchQueue.main.async {
                    self?.updateUI(uuid, text)
                }
            }
        }
    }
    
    func handleTouchEnded() {
        // 4. 结束操作也进串行队列
        audioQueue.async { [weak self] in
            HKAITool.shared.sendFinished()
            
            DispatchQueue.main.async {
                self?.chatingBarPressStatus = .None
            }
        }
    }
}

执行时序对比 #

并发队列(崩溃) #

操作1 ──[Thread A]──┬──[执行]──┬──[释放资源]
                    │          │
操作2 ──[Thread B]──┴──[执行]──┴──[访问已释放资源] 💥

串行队列(安全) #

操作1 ──[执行]──[完成]──[释放资源]──┐
                                   ↓
操作2 ──────────[等待]──────────[执行]──[完成]

完整实现示例 #

import UIKit
import AVFoundation

class AudioManager {
    // 单例模式
    static let shared = AudioManager()
    
    // 串行队列保护所有音频操作
    private let audioQueue = DispatchQueue(
        label: "com.app.audioQueue",
        qos: .userInitiated
    )
    
    // 防止并发的标志
    private var isProcessing = false
    
    func startRecording(completion: @escaping (Bool) -> Void) {
        audioQueue.async { [weak self] in
            guard let self = self else { return }
            
            // 检查是否正在处理
            guard !self.isProcessing else {
                DispatchQueue.main.async {
                    completion(false)
                }
                return
            }
            
            self.isProcessing = true
            
            // 停止之前的音频
            self.stopCurrentAudio()
            
            // 配置音频会话
            do {
                let session = AVAudioSession.sharedInstance()
                try session.setCategory(.playAndRecord, mode: .default)
                try session.setActive(true)
                
                // 创建录音器...
                // self.audioRecorder = ...
                
                DispatchQueue.main.async {
                    completion(true)
                }
            } catch {
                self.isProcessing = false
                DispatchQueue.main.async {
                    completion(false)
                }
            }
        }
    }
    
    func stopRecording() {
        audioQueue.async { [weak self] in
            self?.audioRecorder?.stop()
            self?.isProcessing = false
        }
    }
    
    private func stopCurrentAudio() {
        // 必须在 audioQueue 中调用
        dispatchPrecondition(condition: .onQueue(audioQueue))
        
        // 安全地停止音频
        audioRecorder?.stop()
        audioPlayer?.stop()
    }
}

三层防护机制 #

class SafeAudioCoordinator {
    // 第一层:防抖
    private var lastTouchTime: TimeInterval = 0
    
    // 第二层:状态标志
    private var isProcessing = false
    
    // 第三层:串行队列
    private let audioQueue = DispatchQueue(label: "audio.serial")
    
    func handleTouch() {
        // 1. 防抖检查
        let now = Date().timeIntervalSince1970
        guard now - lastTouchTime > 0.2 else { return }
        lastTouchTime = now
        
        // 2. 状态检查
        guard !isProcessing else { return }
        isProcessing = true
        
        // 3. 串行执行
        audioQueue.async { [weak self] in
            self?.performAudioOperation()
            self?.isProcessing = false
        }
    }
}

关键要点 #

1. 为什么必须串行 #

  • AudioUnit 不是线程安全的 - Apple 文档明确说明
  • 资源独占 - 音频硬件同时只能一个操作
  • 状态机 - 音频系统是状态机,并发会破坏状态

2. 队列选择 #

// ❌ 错误:全局队列
DispatchQueue.global()  // 并发,会创建多线程

// ❌ 错误:主队列
DispatchQueue.main  // 会阻塞 UI

// ✅ 正确:自定义串行队列
DispatchQueue(label: "audio.serial")  // 串行,不阻塞 UI

3. 性能影响 #

方案响应速度安全性CPU占用
并发队列❌ 崩溃
主队列✅ 安全
串行队列✅ 安全

调试技巧 #

验证串行执行 #

private func debugQueue() {
    // 确保在正确的队列
    dispatchPrecondition(condition: .onQueue(audioQueue))
    
    // 打印当前队列
    let label = String(cString: __dispatch_queue_get_label(nil))
    print("Current queue: \(label)")
}

检测并发问题 #

class ConcurrencyDetector {
    private var operationCount = 0
    
    func beginOperation() {
        audioQueue.async {
            self.operationCount += 1
            assert(self.operationCount == 1, "并发检测到!")
        }
    }
    
    func endOperation() {
        audioQueue.async {
            self.operationCount -= 1
        }
    }
}

总结 #

串行队列是解决 AudioUnit 崩溃的核心方案:

  1. 创建专用队列 - 不用全局队列
  2. 所有音频操作进队列 - 包括开始和结束
  3. UI 操作回主线程 - 保持响应性

“并发是万恶之源,串行是可靠之本” - 对于音频这种底层资源尤其如此。

参考资料 #