使用GPUImage录制视频时首次调用startRecording函数出现卡顿及崩溃问题
使用GPUImage录制视频时首次调用startRecording函数出现卡顿及崩溃问题
看起来你在基于GPUImage开发相机录制功能时,碰到了首次启动录制的卡顿和崩溃难题——尤其是那个提示帧缓冲过度释放的NSInternalInconsistencyException异常,我来帮你拆解问题根源和解决办法:
问题根源分析
卡顿原因:
你当前把UI元素创建(比如containerView、cloneView)、布局计算和GPU录制初始化都挤在录制启动的主队列路径上,GPUImage首次创建GPUImageUIElement、GPUImageMovieWriter时需要加载编码器资源、配置硬件加速上下文,这些同步操作会阻塞UI线程,导致4秒左右的卡顿。崩溃原因:
异常信息明确指向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
相关产品推荐
相关产品推荐

