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

Swift中AVFoundation视频帧多线程处理实现 解决预览卡顿问题

iOS AVFoundation 多线程帧处理方案落地实现

核心问题分析

你当前的卡顿主要来自两个原因:

  • 摄像头采集回调直接绑定了主队列,所有帧转换、处理逻辑都占用主线程资源
  • 帧处理耗时过长阻塞主线程,导致UI渲染、系统响应都被卡住

改造方案实现

我们通过「采集队列生产帧→缓冲队列控流→处理队列消费帧→主队列回调UI」的四层结构实现多线程方案,完全避免主线程阻塞。

第一步:改造CaptureManager,新增队列与缓冲配置

首先在CaptureManager类中新增私有属性,替换原有采集队列:

class CaptureManager: NSObject {
    internal static let shared = CaptureManager()
    weak var delegate: CaptureManagerDelegate?
    var session: AVCaptureSession?
    var isBackCamera = true
    
    // 新增属性
    private let frameBuffer = NSMutableArray()
    private let bufferLock = NSLock()
    private let maxBufferSize = 2 // 缓冲队列最多存2帧,避免内存溢出和画面延迟
    private let captureQueue = DispatchQueue(label: "com.yourapp.capture_serial_queue") // 采集专用队列
    private let processQueue = DispatchQueue(label: "com.yourapp.process_serial_queue") // 处理专用队列
    private let callbackQueue = DispatchQueue.main // UI回调专用队列

    // ... 原有其他属性保持不变

然后修改init方法中采集输出的队列绑定,把原来的DispatchQueue.main替换为自定义的采集队列:

// 原来的代码
// output.setSampleBufferDelegate(self, queue: DispatchQueue.main)
// 替换为
output.setSampleBufferDelegate(self, queue: captureQueue)

第二步:实现帧生产消费逻辑

改造captureOutput代理方法作为生产者,同时新增消费逻辑:

extension CaptureManager: AVCaptureVideoDataOutputSampleBufferDelegate {
    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        // 先 retain sampleBuffer 避免被系统提前释放
        guard let buffer = CMSampleBufferCreateCopy(allocator: kCFAllocatorDefault, sampleBuffer: sampleBuffer) else {
            return
        }
        
        bufferLock.lock()
        // 超过缓冲上限就丢弃最早的帧
        if frameBuffer.count >= maxBufferSize {
            let oldBuffer = frameBuffer.removeObject(at: 0) as! CMSampleBuffer
            CFRelease(oldBuffer)
        }
        frameBuffer.add(buffer)
        bufferLock.unlock()
        
        // 唤醒处理队列消费帧
        processQueue.async { [weak self] in
            self?.consumeFrame()
        }
    }
    
    // 帧消费逻辑
    private func consumeFrame() {
        bufferLock.lock()
        guard frameBuffer.count > 0 else {
            bufferLock.unlock()
            return
        }
        let buffer = frameBuffer.removeObject(at: 0) as! CMSampleBuffer
        bufferLock.unlock()
        
        // 帧转换、处理逻辑全部在子线程完成
        guard let outputImage = getImageFromSampleBuffer(sampleBuffer: buffer) else {
            CFRelease(buffer)
            return
        }
        CFRelease(buffer)
        
        // 处理完成后回调到主队列更新UI
        callbackQueue.async { [weak self] in
            self?.delegate?.processCapturedImage(image: outputImage)
        }
    }
}

第三步:优化停止会话逻辑,清空缓冲避免内存泄漏

在stopSession方法中补充缓冲队列清空逻辑:

func stopSession() {
    session?.stopRunning()
    bufferLock.lock()
    frameBuffer.forEach { buffer in
        CFRelease(buffer as! CMSampleBuffer)
    }
    frameBuffer.removeAllObjects()
    bufferLock.unlock()
}

第四步:ViewController层代码无需大幅修改

原有processCapturedImage回调已经自动跑在主队列,直接更新UI即可,重处理逻辑建议提前放到consumeFrame方法中完成,避免占用主线程:

extension ViewController: CaptureManagerDelegate {
    func processCapturedImage(image: UIImage) {
        self.imageView.image = image
        // 仅保留极轻量的UI操作
    }
}

优化建议

  • 如果不需要严格保证帧处理顺序,可以把processQueue改成并发队列提升处理速度,但需要额外处理帧顺序错乱的问题
  • 缓冲队列大小maxBufferSize根据你的处理耗时调整,建议不要超过3,否则会出现明显的画面延迟
  • 如果不需要保留原始sampleBuffer,也可以在采集队列直接转成UIImage再存入缓冲队列,省去手动管理CF对象的步骤

内容的提问来源于stack exchange,提问作者Parham Khamsepour

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.26 10:15:03