DispatchQueue.concurrentPerform中AVAudioConverter缓冲区过度保留致内存飙升
我在为音频分析工具实现并行化处理时,遇到批量处理音频文件堆空间占用持续攀升的问题。排查发现,在DispatchQueue.concurrentPerform块内调用AVAudioConverter.convert(to:error:withInputFrom:)时,每次转换相关的大量内存会被保留至所有并发任务结束,而非单轮迭代完成后释放,导致内存占用不断上升,而非保持稳定。替换转换调用为大内存分配或其他计算时,内存能正常释放,说明问题仅与AVAudioConverter的convert调用行为相关。
简化后的测试代码(实际为处理大量不同文件,此处重复转换同一文件):
let outputFormat = AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: 8000, channels: 1, interleaved: false)! DispatchQueue.global(qos: .userInitiated).async { DispatchQueue.concurrentPerform(iterations: 1000) { index in let inputFile = try! AVAudioFile(forReading: URL(filePath: "/path/to/some/song.m4a")) let inputFormat = inputFile.processingFormat let inputFrameCount = AVAudioFrameCount(inputFile.length) let inputBuffer = AVAudioPCMBuffer(pcmFormat: inputFormat, frameCapacity: inputFrameCount)! try! inputFile.read(into: inputBuffer) let sampleRateCoefficient = inputFormat.sampleRate / outputFormat.sampleRate let outputFrameCount = UInt32(Double(inputFrameCount) / sampleRateCoefficient) let outputBuffer = AVAudioPCMBuffer(pcmFormat: outputFormat, frameCapacity: outputFrameCount)! let converter = AVAudioConverter(from: inputBuffer.format, to: outputFormat)! let converterInputBlock: AVAudioConverterInputBlock = { _, outStatus in outStatus.pointee = AVAudioConverterInputStatus.haveData return inputBuffer } // 替换为其他繁重计算时内存正常释放,仅调用convert时内存保留至所有任务结束 converter.convert( to: outputBuffer, error: nil, withInputFrom: converterInputBlock ) } }
问题根源分析
AVAudioConverter的convert(to:error:withInputFrom:)调用可能存在内部引用循环,或是其内部缓存的资源未在当前迭代结束时及时释放。另外,AVAudioConverterInputBlock捕获了inputBuffer,转换器可能在内部持有该block的引用,导致buffer无法被ARC及时回收。
解决方案尝试
显式打破引用循环:在inputBlock中使用弱引用捕获inputBuffer,避免循环引用导致内存无法释放:
let converterInputBlock: AVAudioConverterInputBlock = { [weak inputBuffer] _, outStatus in guard let buffer = inputBuffer else { outStatus.pointee = .endOfStream return nil } outStatus.pointee = .haveData return buffer }手动触发内存回收:将所有相关对象声明为可选类型,在迭代结束前显式置空,并触发自动释放池回收底层Objective-C对象:
DispatchQueue.concurrentPerform(iterations: 1000) { index in var inputFile: AVAudioFile? var inputBuffer: AVAudioPCMBuffer? var outputBuffer: AVAudioPCMBuffer? var converter: AVAudioConverter? do { inputFile = try AVAudioFile(forReading: URL(filePath: "/path/to/some/song.m4a")) let inputFormat = inputFile!.processingFormat let inputFrameCount = AVAudioFrameCount(inputFile!.length) inputBuffer = AVAudioPCMBuffer(pcmFormat: inputFormat, frameCapacity: inputFrameCount)! try inputFile!.read(into: inputBuffer!) let sampleRateCoefficient = inputFormat.sampleRate / outputFormat.sampleRate let outputFrameCount = UInt32(Double(inputFrameCount) / sampleRateCoefficient) outputBuffer = AVAudioPCMBuffer(pcmFormat: outputFormat, frameCapacity: outputFrameCount)! converter = AVAudioConverter(from: inputBuffer!.format, to: outputFormat)! let converterInputBlock: AVAudioConverterInputBlock = { _, outStatus in outStatus.pointee = .haveData return inputBuffer } converter?.convert( to: outputBuffer!, error: nil, withInputFrom: converterInputBlock ) } catch { // 处理错误 } // 显式置空触发ARC回收 inputFile = nil inputBuffer = nil outputBuffer = nil converter = nil // 强制回收底层自动释放对象 autoreleasepool { } }限制并发任务数量:
concurrentPerform会根据系统核心数创建尽可能多的并发任务,易导致内存过载。改用带信号量的自定义并发队列,限制最大并发数:let converterQueue = DispatchQueue(label: "com.audio.converter", attributes: .concurrent, target: .global(qos: .userInitiated)) let semaphore = DispatchSemaphore(value: 4) // 限制最大4个并发任务 for index in 0..<1000 { converterQueue.async { semaphore.wait() defer { semaphore.signal() } // 放置原迭代内的音频转换代码 } }调整转换器配置:尝试显式设置
AVAudioConverter的maximumOutputPacketSize参数,减少不必要的内存缓存,避免资源长期占用。
内容的提问来源于stack exchange,提问作者ibsh

