iOS端从PHAsset上传大文件至Google可恢复服务是否需复制到临时文件?
问题解答
1. 是否需要将PHAsset复制到临时文件再上传?
是的,必须这么做。你遇到的沙箱权限错误,是因为PhotoKit返回的系统相册文件URL(/var/mobile/Media/DCIM/...)属于系统沙箱范畴,你的App后台Session没有权限直接访问这些文件。只有将Asset导出到App自身的沙箱目录(临时目录或Documents子目录),才能让URLSession获得合法的访问权限。
2. GB级视频文件的磁盘空间管理方案
(1)优先选择「流式分块上传」,避免复制整个文件
对于大体积视频,最优方案是不复制完整文件到本地,直接通过PHAssetResourceManager读取Asset的数据流,结合Google可恢复上传的分块机制,分块读取并上传。这种方式完全不会占用额外磁盘空间,从根源上避免空间占用问题。
(2)若必须复制文件,做好以下空间管控
- 复制前先做空间校验:计算文件大小,对比设备剩余空间,预留至少10%的冗余空间(防止上传过程中系统临时占用空间导致不足)
- 使用临时目录存储:将文件导出到
NSTemporaryDirectory(),系统会在设备空间不足时自动清理临时文件,但上传完成/失败后必须手动删除,避免长期占用 - 上传完成立即清理:在URLSession任务的
completionHandler中主动删除临时文件 - 控制并发上传数:避免同时导出多个大文件,防止磁盘空间骤降
3. 极端空间不足的处理方案
- 上传前拦截:检查剩余空间不足时,直接提示用户释放设备空间,终止上传流程
- 上传过程中监控:通过
NSNotificationCenter监听NSFileSystemSizeDidChangeNotification,实时监控磁盘空间,一旦剩余空间低于阈值(比如小于当前上传块大小的2倍),立即暂停/终止上传,清理已生成的临时文件 - 提供用户引导:提示用户删除无用照片、视频或卸载不常用App,释放空间后再尝试上传
代码修改示例
(1)导出Asset到临时文件的方法
func exportPHAssetToTempFile(assetIdentifier: String, completion: @escaping (URL?) -> Void) { guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetIdentifier], options: nil).firstObject else { completion(nil) return } // 生成唯一临时文件名 let tempDir = NSTemporaryDirectory() let fileExtension = asset.mediaType == .video ? ".mov" : ".jpg" let tempFileUrl = URL(fileURLWithPath: tempDir).appendingPathComponent("\(UUID().uuidString)\(fileExtension)") let resourceOptions = PHAssetResourceRequestOptions() resourceOptions.isNetworkAccessAllowed = true // 允许从iCloud下载 guard let resource = PHAssetResource.assetResources(for: asset).first else { completion(nil) return } // 导出Asset到临时文件 PHAssetResourceManager.default().writeData(for: resource, toFile: tempFileUrl, options: resourceOptions) { error in if let error = error { print("导出Asset失败: \(error.localizedDescription)") try? FileManager.default.removeItem(at: tempFileUrl) completion(nil) } else { completion(tempFileUrl) } } }
(2)空间检查与上传流程
extension ExampleViewController: PHPickerViewControllerDelegate { public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { picker.dismiss(animated: true) let identifiers = results.compactMap { $0.assetIdentifier } identifiers.forEach { identifier in exportPHAssetToTempFile(assetIdentifier: identifier) { tempFileUrl in guard let tempFileUrl = tempFileUrl else { // 提示用户导出失败 return } // 检查剩余空间是否足够 guard self.hasEnoughSpace(forFileAt: tempFileUrl) else { try? FileManager.default.removeItem(at: tempFileUrl) // 提示用户设备空间不足 return } // 构建Google上传请求 guard let uploadUrl = URL(string: googleSessionUriToUpload) else { return } var request = URLRequest(url: uploadUrl) request.httpMethod = "PUT" // 创建上传任务,完成后清理临时文件 let task = URLSession.shared.uploadTask(with: request, fromFile: tempFileUrl) { _, _, error in // 无论成功失败,都删除临时文件 try? FileManager.default.removeItem(at: tempFileUrl) if let error = error { print("上传失败: \(error.localizedDescription)") // 提示用户上传失败 } else { // 提示用户上传成功 } } task.resume() } } } // 检查剩余空间是否足够 private func hasEnoughSpace(forFileAt url: URL, extraReserve: Int64 = 1024 * 1024 * 100) -> Bool { do { let fileAttributes = try FileManager.default.attributesOfItem(atPath: url.path) let fileSize = fileAttributes[.size] as? Int64 ?? 0 let fsAttributes = try FileManager.default.attributesOfFileSystem(forPath: NSTemporaryDirectory()) let freeSpace = fsAttributes[.systemFreeSize] as? Int64 ?? 0 // 预留100MB额外空间,避免上传过程中空间不足 return freeSpace >= fileSize + extraReserve } catch { print("空间检查失败: \(error.localizedDescription)") return false } } }
(3)流式分块上传思路(核心伪代码)
func uploadAssetInChunks(assetIdentifier: String, sessionUri: String) { guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetIdentifier], options: nil).firstObject else { return } guard let resource = PHAssetResource.assetResources(for: asset).first else { return } let chunkSize: Int64 = 10 * 1024 * 1024 // 10MB每块 var offset: Int64 = 0 var currentChunkData = Data() let options = PHAssetResourceRequestOptions() options.isNetworkAccessAllowed = true // 读取Asset数据流,分块上传 PHAssetResourceManager.default().requestData(for: resource, options: options) { data in currentChunkData.append(data) // 当累计数据达到分块大小,上传该块 if currentChunkData.count >= chunkSize { self.uploadChunk(data: currentChunkData, offset: offset, sessionUri: sessionUri) offset += Int64(currentChunkData.count) currentChunkData.removeAll() } } completionHandler: { error in if let error = error { print("读取Asset数据失败: \(error.localizedDescription)") } else { // 上传最后一块剩余数据 if !currentChunkData.isEmpty { self.uploadChunk(data: currentChunkData, offset: offset, sessionUri: sessionUri) } // 标记上传完成 } } } // 上传单个分块到Google可恢复会话 private func uploadChunk(data: Data, offset: Int64, sessionUri: String) { guard let url = URL(string: sessionUri) else { return } var request = URLRequest(url: url) request.httpMethod = "PUT" request.setValue("bytes \(offset)-\(offset + Int64(data.count) - 1)/\(totalFileSize)", forHTTPHeaderField: "Content-Range") let task = URLSession.shared.uploadTask(with: request, from: data) { _, response, error in // 处理分块上传响应,更新上传进度 } task.resume() }
内容的提问来源于stack exchange,提问作者hjh5696
相关产品推荐
相关产品推荐

