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

PHPhotoLibrary performChanges批量复制图片视频报错排查

报错原因
  • 核心诱因是并发写入冲突:PHPhotoLibrary的变更操作本身不是线程安全的,原代码把每个文件的保存任务都直接扔到全局并发队列执行,会同时对同一个自定义相册触发多个performChanges请求。多个变更块同时操作同一个PHAssetCollectionChangeRequest时会产生资源竞争,照片库写入底层是串行调度的,并发提交的请求会互相打断:
    • 错误码-1是写入冲突导致的通用操作失败
    • 错误码3302是并发写入时系统临时无法读取源文件(视频文件校验读取时间更长,这类错误在视频文件上出现概率更高)
  • 代码逻辑缺陷:原代码在performChanges的changes闭包内部直接调用completion回调,这个闭包运行在照片库的内部私有队列,提前return会直接中断变更流程,还会导致回调时序混乱,出现状态不一致。
  • 逻辑遗漏:原代码保存成功的分支没有触发completion回调,会导致调用方无法收到成功状态。
修复方案
  1. 创建专门的自定义串行队列处理所有照片库写入操作,保证所有变更请求按顺序逐个提交执行,从根源上避免并发冲突。
  2. 把所有回调逻辑移到performChanges的完成闭包中处理,不在changes块内做业务回调。
  3. 用信号量保证串行队列里的上一个写入任务完全结束后,再执行下一个任务。
  4. 补全成功分支的回调逻辑。

修复后的单文件保存代码如下:

// 自定义照片写入串行队列,替换原有的全局并发队列调度
private let photoSaveSerialQueue = DispatchQueue(label: "com.app.photoSave", qos: .background)

public func saveFileToPhotoLibrary(_ url: URL, completion: @escaping (Bool) -> Void)
{
    guard let album = _album else {
        DispatchQueue.main.async { completion(false) }
        return
    }

    guard let supertype = url.supertype, supertype == .image || supertype == .movie else {
        DispatchQueue.main.async { completion(false) }
        return
    }

    photoSaveSerialQueue.async {
        var saveResult = false
        let waitSemaphore = DispatchSemaphore(value: 0)

        PHPhotoLibrary.shared().performChanges({
            // 仅在changes块内处理资源创建和相册关联逻辑,不做回调
            let changeRequest: PHAssetChangeRequest?
            if supertype == .image {
                changeRequest = PHAssetChangeRequest.creationRequestForAssetFromImage(atFileURL: url)
            } else {
                changeRequest = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: url)
            }

            guard let assetRequest = changeRequest,
                  let placeholder = assetRequest.placeholderForCreatedAsset,
                  let albumRequest = PHAssetCollectionChangeRequest(for: album) else {
                return
            }
            albumRequest.addAssets([placeholder] as NSFastEnumeration)
        }) { success, error in
            if let err = error {
                print("保存\(url.lastPathComponent)失败:\(err.localizedDescription)")
                saveResult = false
            } else {
                saveResult = success
            }
            waitSemaphore.signal()
        }
        // 等待当前文件保存流程完全结束,再放行队列下一个任务
        waitSemaphore.wait()
        DispatchQueue.main.async { completion(saveResult) }
    }
}

如果是批量保存场景,更推荐把同批次所有文件放到同一个performChanges块中处理,执行效率更高,也完全不会出现并发冲突,示例代码如下:

public func saveBatchFilesToAlbum(_ fileUrls: [URL], completion: @escaping (Bool, [URL]) -> Void) {
    guard let album = _album else {
        DispatchQueue.main.async { completion(false, []) }
        return
    }

    photoSaveSerialQueue.async {
        var successUrls = [URL]()
        let waitSemaphore = DispatchSemaphore(value: 0)

        PHPhotoLibrary.shared().performChanges({
            guard let albumChangeRequest = PHAssetCollectionChangeRequest(for: album) else {
                return
            }
            var placeholders = [PHObjectPlaceholder]()
            for url in fileUrls {
                guard let supertype = url.supertype else { continue }
                let request: PHAssetChangeRequest?
                switch supertype {
                case .image:
                    request = PHAssetChangeRequest.creationRequestForAssetFromImage(atFileURL: url)
                case .movie:
                    request = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: url)
                default:
                    request = nil
                }
                if let req = request, let placeholder = req.placeholderForCreatedAsset {
                    placeholders.append(placeholder)
                    successUrls.append(url)
                }
            }
            if !placeholders.isEmpty {
                albumChangeRequest.addAssets(placeholders as NSFastEnumeration)
            }
        }) { success, error in
            if error != nil {
                successUrls.removeAll()
            }
            waitSemaphore.signal()
        }

        waitSemaphore.wait()
        DispatchQueue.main.async { completion(!successUrls.isEmpty, successUrls) }
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 02:51:25