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
}
}
}
问题剖析 #
- 并发执行 - 多个线程同时操作 AudioUnit
- 资源竞争 - graph 被多次释放
- 状态不一致 - 音频系统内部状态混乱
串行队列解决方案 #
核心改造 #
// ✅ 安全:专用串行队列
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 崩溃的核心方案:
- 创建专用队列 - 不用全局队列
- 所有音频操作进队列 - 包括开始和结束
- UI 操作回主线程 - 保持响应性
“并发是万恶之源,串行是可靠之本” - 对于音频这种底层资源尤其如此。