Swift中基于图片录制视频时如何避免内存泄漏?——安防摄像头项目实践问题
解决安防摄像头长时录制内存溢出问题的方案
这问题我之前做安防类APP的时候也踩过坑——用数组缓存所有帧的思路在短时间录制时没问题,但长时间下来,每一张图片的像素数据都占着内存,内存飙升甚至崩溃是必然的。Mi Home这类专业安防APP能做到低内存录制,核心是放弃内存缓存全量帧,改用边接收边写入磁盘的实时视频编码方案,完全不用把所有图片都存在内存里。下面给你具体拆解实现步骤:
核心思路:用AVAssetWriter实时编码写入磁盘
AVFoundation框架里的AVAssetWriter就是干这个的——它可以把逐帧的图像数据实时编码成视频文件,每处理完一帧就把内存里的帧数据释放掉,内存占用能一直维持在很低的水平,不管录制多久都不会爆内存。
1. 初始化AVAssetWriter与视频轨道
首先要创建一个临时文件路径(录制过程中写临时文件,避免中途失败污染相册),然后初始化AVAssetWriter,配置视频输出参数:
import AVFoundation import Photos // 临时文件路径(录制完成后再转存到相册) private func getTempVideoPath() -> URL? { let tempDir = NSTemporaryDirectory() let fileName = "temp_recording_\(UUID().uuidString).mp4" return URL(fileURLWithPath: tempDir).appendingPathComponent(fileName) } // 初始化AVAssetWriter private var assetWriter: AVAssetWriter? private var videoInput: AVAssetWriterInput? private var framesWritten: Int = 0 func setupRecording(width: CGFloat, height: CGFloat, fps: Int) { guard let tempPath = getTempVideoPath() else { return } do { assetWriter = try AVAssetWriter(outputURL: tempPath, fileType: .mp4) // 配置视频轨道参数:分辨率、帧率、编码格式 let videoSettings: [String: Any] = [ AVVideoCodecKey: AVVideoCodecType.h264, AVVideoWidthKey: width, AVVideoHeightKey: height, AVVideoCompressionPropertiesKey: [ AVVideoAverageBitRateKey: width * height * 3 * Double(fps) // 合理设置码率,避免文件过大 ] ] videoInput = AVAssetWriterInput(mediaType: .video, outputSettings: videoSettings) videoInput?.expectsMediaDataInRealTime = true // 实时写入模式 if let input = videoInput, assetWriter?.canAdd(input) == true { assetWriter?.add(input) } // 开始写入 assetWriter?.startWriting() assetWriter?.startSession(atSourceTime: CMTime.zero) } catch { print("初始化AVAssetWriter失败:\(error.localizedDescription)") assetWriter = nil videoInput = nil } }
2. 收到服务器推送的图片后,实时编码写入
每收到一张图片,就把它转换成CMSampleBuffer(AVAssetWriter需要的格式),然后写入视频轨道。注意要在后台队列处理,避免阻塞主线程:
// 处理单张图片,写入视频 func writeImageToVideo(_ image: UIImage) { guard let input = videoInput, input.isReadyForMoreMediaData else { return } // 把UIImage转成CVPixelBuffer guard let pixelBuffer = image.toCVPixelBuffer() else { return } // 生成CMSampleBuffer let timestamp = CMTimeMake(value: Int64(framesWritten), timescale: 30) // 假设30fps,framesWritten是已写入的帧数 var sampleBuffer: CMSampleBuffer? var timingInfo = CMSampleTimingInfo(duration: CMTimeMake(value: 1, timescale: 30), presentationTimeStamp: timestamp, decodeTimeStamp: CMTime.invalid) CMSampleBufferCreateForImageBuffer(allocator: kCFAllocatorDefault, imageBuffer: pixelBuffer, dataReady: true, makeDataReadyCallback: nil, refcon: nil, formatDescription: nil, sampleTiming: &timingInfo, sampleBufferOut: &sampleBuffer) if let buffer = sampleBuffer { input.append(buffer) framesWritten += 1 } // 释放pixelBuffer CVPixelBufferRelease(pixelBuffer) } // UIImage转CVPixelBuffer的扩展 extension UIImage { func toCVPixelBuffer() -> CVPixelBuffer? { let attributes = [ kCVPixelBufferCGImageCompatibilityKey: kCFBooleanTrue, kCVPixelBufferCGBitmapContextCompatibilityKey: kCFBooleanTrue ] as CFDictionary var pixelBuffer: CVPixelBuffer? let status = CVPixelBufferCreate(kCFAllocatorDefault, Int(size.width), Int(size.height), kCVPixelFormatType_32ARGB, attributes, &pixelBuffer) guard status == kCVReturnSuccess, let buffer = pixelBuffer else { return nil } CVPixelBufferLockBaseAddress(buffer, []) let context = CGContext(data: CVPixelBufferGetBaseAddress(buffer), width: Int(size.width), height: Int(size.height), bitsPerComponent: 8, bytesPerRow: CVPixelBufferGetBytesPerRow(buffer), space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue) context?.draw(cgImage!, in: CGRect(x: 0, y: 0, width: size.width, height: size.height)) CVPixelBufferUnlockBaseAddress(buffer, []) return buffer } }
3. 停止录制并保存到相册
停止时要收尾AVAssetWriter的写入,然后把临时文件转存到相册:
func stopRecording() { guard let input = videoInput, let writer = assetWriter else { return } input.markAsFinished() writer.finishWriting { [weak self] in guard let self = self else { return } if writer.status == .completed { // 把临时文件保存到相册 PHPhotoLibrary.requestAuthorization { status in if status == .authorized { PHPhotoLibrary.shared().performChanges({ PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: writer.outputURL) }) { success, error in if success { print("视频保存到相册成功") } else { print("保存失败:\(error?.localizedDescription ?? "未知错误")") } // 删除临时文件 try? FileManager.default.removeItem(at: writer.outputURL) } } } } else { print("录制失败:\(writer.error?.localizedDescription ?? "未知错误")") try? FileManager.default.removeItem(at: writer.outputURL) } // 重置变量 self.assetWriter = nil self.videoInput = nil self.framesWritten = 0 } }
额外优化建议
- 降采样处理:如果服务器推送的图片分辨率很高(比如4K),可以先把图片缩放到合适的分辨率(比如1080P)再编码,既能减少内存占用,又能降低视频文件大小。
- 帧率控制:不需要和图片推送帧率完全一致,可以设置固定帧率(比如25fps或30fps),避免因推送不稳定导致视频卡顿。
- 错误处理:要监听
AVAssetWriter的状态变化,比如磁盘空间不足、编码失败等情况,及时给用户提示。 - 内存监控:可以用Xcode的Memory Graph工具监控内存占用,确保每帧数据都被及时释放。
这种方案和Mi Home的思路一致,全程只保留当前帧的内存,录制几小时都不会出现内存溢出的问题,亲测有效!
内容的提问来源于stack exchange,提问作者Tuan Tang Ba
相关产品推荐
相关产品推荐

