如何在宿主App中跟踪共享扩展创建的后台上传任务进度并管控任务
解决方案
一、先解决-996错误(核心前提)
错误NSURLErrorBackgroundSessionWasDisconnected(-996)的本质是同一个后台Session ID被多个进程(宿主App/共享扩展)同时创建了活跃实例。你当前代码中宿主主动创建了扩展专属的backgroundSessionExtension,这会和共享扩展中正在运行的同ID Session直接冲突。
修正规则
- 共享扩展:仅创建并使用专属的后台Session ID(如
bgSessionIdExtension) - 宿主App:绝对不能主动创建扩展用的Session,而是通过系统API获取该ID对应的任务(系统会自动关联已存在的Session,无需手动实例化)
修改后的宿主端Session代码
// 宿主仅维护自身的上传Session,移除扩展Session的手动创建代码 private lazy var backgroundSession: URLSession = { return createBackgroundURLSession(with: FileUploadService.bgSessionId) }()
二、宿主跟踪共享扩展的上传进度
共享扩展和宿主是独立进程,后台Session的代理回调只会触发在任务发起进程(扩展),因此需要通过跨进程数据同步实现进度跟踪。
最优实现:共享容器+Realm跨进程同步
- 确认共享容器配置:确保宿主和扩展已开启App Groups权限,且使用同一个
sharedContainerIdentifier(你当前代码已配置,无需修改) - Realm存储任务元数据:在共享容器中创建Realm数据库,存储每个上传任务的核心信息:
- Session ID
- 系统Task ID(
task.taskIdentifier) - 上传进度(已上传字节/总字节)
- 任务状态(等待中/上传中/已完成/已取消/失败)
- 扩展端实时更新进度:在共享扩展的
URLSessionTaskDelegate回调中,同步更新Realm数据:// 扩展端代理方法:更新上传进度 func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { guard totalBytesExpectedToSend > 0 else { return } let progress = Float(totalBytesSent) / Float(totalBytesExpectedToSend) // 初始化共享容器的Realm配置 let sharedContainerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: AppConstants.appGroupName)! let realmConfig = Realm.Configuration(fileURL: sharedContainerURL.appendingPathComponent("UploadTasks.realm")) do { let realm = try Realm(configuration: realmConfig) try realm.write { // 找到对应任务并更新进度 if let uploadTask = realm.objects(UploadTask.self).filter("taskId == %@", task.taskIdentifier).first { uploadTask.progress = progress uploadTask.status = .uploading } } } catch { print("Realm更新进度失败:\(error)") } } - 宿主端监听进度变化:宿主通过Realm的
NotificationToken监听共享容器中任务数据的实时变化,同步更新UI:private var realmNotificationToken: NotificationToken? func setupProgressMonitor() { let sharedContainerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: AppConstants.appGroupName)! let realmConfig = Realm.Configuration(fileURL: sharedContainerURL.appendingPathComponent("UploadTasks.realm"), readOnly: true) do { let realm = try Realm(configuration: realmConfig) let uploadTasks = realm.objects(UploadTask.self) // 监听任务数据变化 realmNotificationToken = uploadTasks.observe { [weak self] changes in switch changes { case .initial, .update: // 更新UI展示最新进度 self?.refreshProgressUI() case .error(let error): print("Realm监听错误:\(error)") } } } catch { print("Realm初始化失败:\(error)") } } // 页面销毁时销毁监听,避免内存泄漏 deinit { realmNotificationToken?.invalidate() }
三、宿主对扩展任务执行取消/暂停操作
核心逻辑:通过系统API获取扩展的Session任务,直接调用任务的原生方法。
实现代码
// 统一处理扩展任务的工具方法 func handleExtensionUploadTasks(action: @escaping (URLSessionUploadTask) -> Void) { URLSession.shared.getTasksWithCompletionHandler { _, uploadTasks, _ in // 筛选出扩展专属Session的任务 let extensionTasks = uploadTasks.filter { $0.session.configuration.identifier == FileUploadService.bgSessionIdExtension } extensionTasks.forEach(action) } } // 调用示例:取消所有扩展上传任务 func cancelAllExtensionTasks() { handleExtensionUploadTasks { $0.cancel() } } // 调用示例:暂停所有扩展上传任务 func suspendAllExtensionTasks() { handleExtensionUploadTasks { $0.suspend() } } // 调用示例:恢复所有暂停的扩展上传任务 func resumeAllExtensionTasks() { handleExtensionUploadTasks { $0.resume() } }
注意事项
- 暂停任务后,系统会保留任务状态,重启App/扩展时可通过
getTasksWithCompletionHandler恢复任务,调用resume()即可继续上传 - 取消任务后,任务会从系统后台Session中移除,需同步更新Realm中对应任务的状态为「已取消」
四、补充优化建议
- 任务唯一标识:每个上传任务创建时,额外生成自定义UUID存储到Realm,避免系统
taskIdentifier重复导致的匹配错误 - 错误同步:在扩展端的
urlSession(_:task:didCompleteWithError:)回调中,同步更新Realm中任务的错误信息和最终状态 - 内存优化:宿主端的Realm监听
NotificationToken必须在页面销毁时销毁,避免内存泄漏
内容的提问来源于stack exchange,提问作者Ferenc
相关产品推荐
相关产品推荐

