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
相关产品推荐
相关产品推荐

