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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.30 11:47:48