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

