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

iOS中在不阻塞UI线程的情况下将图片转为视频(Swift)

嘿,我明白你现在卡在把图片快照转视频时UI被锁的问题上了——这种主线程阻塞的情况确实让人头疼,尤其是试了DispatchQueues还没解决的时候。咱们一步步拆解问题,找到根因和解决方案:

解决图片转视频时UI线程锁定的问题

首先得明确:图片编码、视频写入这类操作都是耗时的IO/计算任务,必须彻底从主线程剥离。你说试了队列没用,大概率是队列的使用姿势不对,比如误把任务丢回主线程,或者用了同步调用导致主线程等待。

先排查你可能踩过的误区

  • 是不是不小心用了DispatchQueue.main.async?这反而会把任务扔回主线程,肯定锁UI。
  • 有没有用sync调用?同步执行会让当前线程(主线程)一直等待任务完成,直接导致阻塞。

正确的实现思路:把UI和耗时任务彻底分离

咱们把流程拆成主线程处理交互和后台执行核心任务两部分:

1. 用户触发操作(仅在主线程做UI更新)

比如按钮点击事件里,只做参数准备和UI状态切换,然后把核心转视频任务丢到后台队列:

@IBAction func startVideoConversion(_ sender: UIButton) {
    // 主线程只做UI状态更新(比如禁用按钮、显示加载动画)
    sender.isEnabled = false
    loadingIndicator.startAnimating()
    
    // 准备好要转换的图片数组、视频输出路径
    let targetImages = yourImageSnapshotArray
    let outputVideoURL = getVideoOutputURL()
    
    // 把耗时任务丢到后台队列,用async避免阻塞主线程
    DispatchQueue.global(qos: .userInitiated).async {
        self.convertImagesToVideo(images: targetImages, outputURL: outputVideoURL) { success in
            // 任务完成后切回主线程更新UI
            DispatchQueue.main.async {
                self.loadingIndicator.stopAnimating()
                sender.isEnabled = true
                // 给用户反馈结果
                let alertTitle = success ? "转换完成" : "转换失败"
                let alert = UIAlertController(title: alertTitle, message: nil, preferredStyle: .alert)
                alert.addAction(UIAlertAction(title: "确定", style: .default))
                self.present(alert, animated: true)
            }
        }
    }
}

2. 核心转视频逻辑(完全在后台执行)

这里要确保视频写入、像素转换的所有操作都在后台队列,绝对不碰主线程。基于你参考的教程思路,调整后的核心代码如下:

func convertImagesToVideo(images: [UIImage], outputURL: URL, completion: @escaping (Bool) -> Void) {
    guard let videoWriter = try? AVAssetWriter(outputURL: outputURL, fileType: .mov) else {
        completion(false)
        return
    }
    
    // 视频基础配置(分辨率、编码格式)
    let videoSettings: [String: Any] = [
        AVVideoCodecKey: AVVideoCodecType.h264,
        AVVideoWidthKey: images.first?.size.width ?? 1080,
        AVVideoHeightKey: images.first?.size.height ?? 1920
    ]
    
    let videoWriterInput = AVAssetWriterInput(mediaType: .video, outputSettings: videoSettings)
    videoWriterInput.expectsMediaDataInRealTime = false // 非实时写入,适合图片转视频场景
    videoWriter.add(videoWriterInput)
    
    // 像素缓冲区适配器,用于把UIImage转成视频帧
    let pixelBufferAdaptor = AVAssetWriterInputPixelBufferAdaptor(
        assetWriterInput: videoWriterInput,
        sourcePixelBufferAttributes: [
            kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32ARGB,
            kCVPixelBufferWidthKey as String: images.first?.size.width ?? 1080,
            kCVPixelBufferHeightKey as String: images.first?.size.height ?? 1920
        ]
    )
    
    videoWriter.startWriting()
    videoWriter.startSession(atSourceTime: .zero)
    
    // 创建单独的队列处理视频写入,避免和全局队列冲突
    let writingQueue = DispatchQueue(label: "com.yourApp.video-writing-queue")
    videoWriterInput.requestMediaDataWhenReady(on: writingQueue) {
        let frameDuration = CMTimeMake(value: 1, timescale: 30) // 设置帧率为30fps
        var currentFrameTime = CMTime.zero
        
        for image in images {
            // 等待输入端口准备好接收数据
            while !videoWriterInput.isReadyForMoreMediaData {
                Thread.sleep(forTimeInterval: 0.01)
            }
            
            // 把UIImage转成CVPixelBuffer
            guard let pixelBuffer = self.createPixelBuffer(from: image) else {
                videoWriter.cancelWriting()
                completion(false)
                return
            }
            
            // 写入视频帧
            pixelBufferAdaptor.append(pixelBuffer, withPresentationTime: currentFrameTime)
            currentFrameTime = CMTimeAdd(currentFrameTime, frameDuration)
        }
        
        // 标记写入完成,结束会话
        videoWriterInput.markAsFinished()
        videoWriter.finishWriting {
            completion(videoWriter.status == .completed)
        }
    }
}

// 辅助函数:将UIImage转换为CVPixelBuffer
private func createPixelBuffer(from image: UIImage) -> CVPixelBuffer? {
    let attrs = [
        kCVPixelBufferCGImageCompatibilityKey: kCFBooleanTrue,
        kCVPixelBufferCGBitmapContextCompatibilityKey: kCFBooleanTrue
    ] as CFDictionary
    
    var pixelBuffer: CVPixelBuffer?
    let status = CVPixelBufferCreate(
        kCFAllocatorDefault,
        Int(image.size.width),
        Int(image.size.height),
        kCVPixelFormatType_32ARGB,
        attrs,
        &pixelBuffer
    )
    
    guard status == kCVReturnSuccess, let buffer = pixelBuffer else {
        return nil
    }
    
    CVPixelBufferLockBaseAddress(buffer, [])
    let context = CGContext(
        data: CVPixelBufferGetBaseAddress(buffer),
        width: Int(image.size.width),
        height: Int(image.size.height),
        bitsPerComponent: 8,
        bytesPerRow: CVPixelBufferGetBytesPerRow(buffer),
        space: CGColorSpaceCreateDeviceRGB(),
        bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue
    )
    
    context?.draw(image.cgImage!, in: CGRect(origin: .zero, size: image.size))
    CVPixelBufferUnlockBaseAddress(buffer, [])
    
    return buffer
}

几个关键注意点

  • 绝对不要在主线程执行视频写入、像素转换:这些操作耗时极长,会直接卡死UI。
  • 用async而非sync:同步调用会让主线程等待后台任务完成,等同于把任务拉回主线程执行。
  • 单独创建写入队列:避免和全局队列的其他任务冲突,保证视频写入的连贯性。

按这个方式调整后,UI应该就能保持流畅,不会再出现锁定的情况了。

内容的提问来源于stack exchange,提问作者Vitor Venturin

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.20 07:09:34