You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

DispatchQueue.concurrentPerform中AVAudioConverter缓冲区过度保留致内存飙升

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 11:47:57