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

使用GPUImage录制视频时首次调用startRecording函数出现卡顿及崩溃问题

使用GPUImage录制视频时首次调用startRecording函数出现卡顿及崩溃问题

看起来你在基于GPUImage开发相机录制功能时,碰到了首次启动录制的卡顿和崩溃难题——尤其是那个提示帧缓冲过度释放的NSInternalInconsistencyException异常,我来帮你拆解问题根源和解决办法:

问题根源分析

  1. 卡顿原因:
    你当前把UI元素创建(比如containerView、cloneView)、布局计算和GPU录制初始化都挤在录制启动的主队列路径上,GPUImage首次创建GPUImageUIElement、GPUImageMovieWriter时需要加载编码器资源、配置硬件加速上下文,这些同步操作会阻塞UI线程,导致4秒左右的卡顿。

  2. 崩溃原因:
    异常信息明确指向Tried to overrelease a framebuffer,核心是帧缓冲的生命周期管理混乱:

    • 你在帧处理完成的回调里才调用videoCamera.useNextFrameForImageCapture(),时机完全不对——这个方法需要在要捕获帧之前调用,而不是帧处理完之后;
    • 后台线程嵌套主队列异步的写法,导致GPU滤镜链配置、录制启动、UI元素更新的时序错乱,线程竞争引发帧缓冲被重复释放;
    • GPUImageUIElement的频繁更新和视频帧捕获的逻辑没有对齐,进一步加剧了帧缓冲的管理冲突。

针对性解决方案

1. 分离计算与UI逻辑,缓解首次启动卡顿

把纯计算的部分(比如裁剪区域计算)移到后台线程,UI相关操作严格放在主队列,但提前复用UI元素,避免每次录制都临时创建:

  • 提前声明一个属性存储containerView,不需要每次录制都重新创建,仅调整尺寸和布局;
  • 将文件删除、裁剪区域计算这类无UI依赖的逻辑放在后台utility队列执行,不占用主队列资源。

2. 修正帧缓冲管理,对齐useNextFrameForImageCapture调用时机

useNextFrameForImageCapture()的作用是告诉GPUCamera要保留下一帧的缓冲,供后续捕获使用,必须在帧生成前调用。同时要避免在帧处理回调里无限制调用,防止过度捕获。

3. 重构线程逻辑,避免嵌套异步的时序混乱

把GPU滤镜链配置、录制启动的核心逻辑统一放在主队列执行,后台仅处理纯计算任务,避免线程嵌套导致的时序冲突。

修改后的代码示例

// 提前在类里声明复用的UI属性
private var containerView: UIView?
private var movieWriter: GPUImageMovieWriter?

func startRecording() {
    let timestamp = Int(Date().timeIntervalSince1970)
    guard let tempDir = NSTemporaryDirectory() as String? else {
        print("Không thể truy cập thư mục tạm")
        return
    }
    movieURL = URL(fileURLWithPath: tempDir.appending("output_\(timestamp).mov"))
    
    // 后台处理文件删除和裁剪区域计算
    DispatchQueue.global(qos: .utility).async {
        // 删除旧文件
        if FileManager.default.fileExists(atPath: self.movieURL!.path) {
            try? FileManager.default.removeItem(at: self.movieURL!)
        }
        
        // 预计算裁剪区域(纯计算,无UI依赖)
        let ratio = self.ratioSelections[self.chooseRatio]
        let outputWidth: CGFloat = 2160
        let outputHeight: CGFloat = outputWidth * CGFloat(ratio.1) / CGFloat(ratio.0)
        let outputSize = CGSize(width: outputWidth, height: outputHeight)
        let inputAspectRatio: CGFloat = 0.55
        let outputAspectRatio = CGFloat(ratio.0) / CGFloat(ratio.1)
        
        var cropRegion = CGRect(x: 0, y: 0, width: 1, height: 1)
        if outputAspectRatio < inputAspectRatio {
            let widthRatio = outputAspectRatio / inputAspectRatio
            let xOffset = (1 - widthRatio) / 2
            cropRegion = CGRect(x: xOffset, y: 0, width: widthRatio, height: 1)
        } else {
            let heightRatio = inputAspectRatio / outputAspectRatio
            let yOffset = (1 - heightRatio) / 2
            cropRegion = CGRect(x: 0, y: yOffset, width: 1, height: heightRatio)
        }
        
        // 计算完成后回到主队列处理GPU配置和UI操作
        DispatchQueue.main.async { [weak self] in
            guard let self = self else { return }
            
            // 清空旧的滤镜目标链
            self.videoCamera.removeAllTargets()
            self.filter.removeAllTargets()
            
            // 创建裁剪滤镜
            guard let cropFilter = GPUImageCropFilter(cropRegion: cropRegion) else {
                print("Không thể tạo crop filter")
                return
            }
            
            // 初始化滤镜链
            let blendFilter = GPUImageAlphaBlendFilter()
            blendFilter.mix = 1.0
            let recordFilter = GPUImageFilter()
            
            // 复用或创建containerView
            let containerFrame = CGRect(x: -10000, y: -10000, width: outputSize.width, height: outputSize.height)
            let currentContainerView: UIView
            if let existingContainer = self.containerView {
                currentContainerView = existingContainer
                currentContainerView.frame = containerFrame
            } else {
                currentContainerView = UIView(frame: containerFrame)
                currentContainerView.backgroundColor = .clear
                let cloneView = self.createLiveScaledCopyView(outputSize: outputSize)
                currentContainerView.addSubview(cloneView)
                self.containerView = currentContainerView // 保存为属性复用
            }
            currentContainerView.setNeedsLayout()
            currentContainerView.layoutIfNeeded()
            
            // 创建UIElement和MovieWriter
            guard let uiElement = GPUImageUIElement(view: currentContainerView) else {
                print("Không thể tạo GPUImageUIElement")
                return
            }
            guard let movieWriter = GPUImageMovieWriter(movieURL: self.movieURL!, size: outputSize) else {
                print("Không thể tạo movie writer")
                return
            }
            movieWriter.encodingLiveVideo = true
            self.movieWriter = movieWriter
            
            // 构建滤镜目标链
            self.videoCamera.addTarget(cropFilter)
            cropFilter.addTarget(recordFilter)
            recordFilter.addTarget(blendFilter)
            uiElement.addTarget(blendFilter)
            blendFilter.addTarget(movieWriter)
            
            // 构建预览链
            let previewFilter = GPUImageFilter()
            cropFilter.addTarget(previewFilter)
            previewFilter.addTarget(self.gpuImageView)
            
            // 修正帧捕获逻辑:启动前先标记要捕获下一帧
            self.videoCamera.useNextFrameForImageCapture()
            
            // 帧处理回调:弱引用避免循环引用,对齐UI更新和帧捕获
            recordFilter.frameProcessingCompletionBlock = { [weak self, weak uiElement] _, _ in
                guard let self = self, let element = uiElement else { return }
                DispatchQueue.main.async {
                    element.update()
                    // 标记下一帧要捕获,避免过度释放
                    self.videoCamera.useNextFrameForImageCapture()
                }
            }
            
            // 配置音频目标
            self.videoCamera.audioEncodingTarget = movieWriter
            
            // 启动录制
            movieWriter.startRecording()
            print("▶️ Started recording video at \(outputSize) with ratio \(ratio.0):\(ratio.1)")
        }
    }
}

额外优化建议

  • 复用滤镜实例:不要每次录制都重新创建GPUImageCropFilter、GPUImageAlphaBlendFilter,提前初始化后仅修改参数(比如cropRegion),减少首次创建的资源开销;
  • 停止录制时的清理:添加停止录制的方法,在停止时调用movieWriter?.finishRecording(),并清空滤镜链、重置useNextFrameForImageCapture的状态,避免残留资源导致的后续录制异常;
  • 弱引用捕获:所有闭包内的self都用[weak self]捕获,避免循环引用导致的内存泄漏和对象生命周期混乱。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.08 12:03:04