SwiftUI周期背景任务问题:通讯录上传DB未重复执行排查
SwiftUI后台任务调度与实现问题
我在SwiftUI中配置了一个每2小时执行一次、将用户通讯录上传至数据库的后台任务,但该任务仅成功执行一次,后续两天未再次运行。现咨询以下问题:
- 每次scenePhase变为.active时调度任务是否会导致过度调度?
- uploadInBackground的实现是否合规?根据WWDC 2022相关内容,若任务未正确取消会降低后台任务优先级。
更新后的代码
@main struct MyApp: SwiftUI.App { var body: some Scene { WindowGroup { Group { if let config = manager.configuration { OpenSyncedRealmView() .environment(\.realmConfiguration, config) } else { AuthRouterView() } } } } } class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { ... BGTaskScheduler.shared.register(forTaskWithIdentifier: "uploadContacts", using: nil) { task in BackgroundTasksService.handleTask(task: task as! BGAppRefreshTask) // Never get inside here } } } class BackgroundTasksService { static func handleTask(task: BGAppRefreshTask) { scheduleBGTask() task.expirationHandler = { task.setTaskCompleted(success: false) } if ((store.permissionStatus == .authorized || store.permissionStatus == .restricted)) { self.uploadInBackground { success in task.setTaskCompleted(success: success) } } else { task.setTaskCompleted(success: true) } } static func scheduleBGTask() { let request = BGProcessingTaskRequest(identifier: uploadContacts) request.earliestBeginDate = Calendar.current.date(byAdding: .hour, value: 2, to: Date()) do { try BGTaskScheduler.shared.submit(request) } catch { print("Unable to submit task: \(error.localizedDescription)") } } static func uploadInBackground( handler: @escaping ((Bool) -> Void) ) { // Fetch Contacts ... // Store locally ... // upload to s3 guard let user_id = UserDefaults.standard.string(forKey: "user_id") else { return } let filename = user_id + ".csv" let postData = FileManager.default.contents(atPath: fileURL.relativePath) API.storage.createUrl(storagePath: "contacts", filename: filename) .onSuccess { createReq in let _ = API.storage.uploadUrl(data: postData!, filename: filename, upload_url: createReq.upload_url, mimeType: .csv) { res in switch res { case .success( _): handler(true) case .failure(let failure): handler(false) } } } } }
我的代码
@main struct MyApp: SwiftUI.App { @Environment(\.scenePhase) private var phase var body: some Scene { WindowGroup { Group { if let config = manager.configuration { OpenSyncedRealmView() .environment(\.realmConfiguration, config) } else { AuthRouterView() } } } .onChange(of: phase) { newPhase in switch newPhase { case .background: BackgroundTasksService.scheduleContactsUpload() default: break } } .backgroundTask(.appRefresh("uploadContacts")) { _ in await BackgroundTasksService.uploadContactsBackgroundTask() } } } class BackgroundTasksService { static func scheduleContactsUpload() { let now = Date.now let inTwoHours = now.addingTimeInterval(60 * 60 * 2) let backgroundTask = BGAppRefreshTaskRequest("uploadContacts") backgroundTask.earliestBeginDate = inTwoHours do { try BGTaskScheduler.shared.submit(backgroundTask) } catch let err { print("Unable to shedule contacts-upload task, error \(err)") } } static func uploadContactsBackgroundTask() async { if store.permissionStatus == .authorized || store.permissionStatus == .restricted { do { try await self.uploadInBackground() } catch { print("failed to finish uploading contacts") } } } func uploadInBackground() async throws { // Fetching Contacts from local phonebook ... // Storing contacts locally ... // uploading to DB guard let user_id = UserDefaults.standard.string(forKey: "user_id") else { return } let filename = user_id + ".csv" guard let postData = FileManager.default.contents(atPath: fileURL.relativePath) else { return } Task(priority: .background) { try await withCheckedThrowingContinuation({ continuation in API.storage.createUrl(storagePath: "contacts", filename: filename) .onSuccess { createReq in let _ = API.storage.uploadUrl(data: postData, filename: filename, upload_url: createReq.upload_url, mimeType: .csv) { res in switch res { case .success( _): continuation.resume() case .failure(let failure): continuation.resume(throwing: failure) } } } .onError({ err in continuation.resume(throwing: err) }) .onCancellation { continuation.resume() } }) } } }
问题解答
1. scenePhase切换时调度任务是否会过度调度?
你的代码是在scenePhase变为.background时调度任务,但即使如此,频繁提交仍会引发问题:
- 重复任务堆积:每次进入后台都提交新任务,系统会自动丢弃旧请求保留最新的,但频繁操作会被判定为滥用后台任务,直接降低后续调度优先级。
- 正确优化方式:
- 提交前先取消同标识符的已有任务,避免重复调度:
static func scheduleContactsUpload() { // 取消已存在的同标识符任务 BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: "uploadContacts") let now = Date.now let inTwoHours = now.addingTimeInterval(60 * 60 * 2) let backgroundTask = BGAppRefreshTaskRequest("uploadContacts") backgroundTask.earliestBeginDate = inTwoHours do { try BGTaskScheduler.shared.submit(backgroundTask) } catch let err { print("Unable to schedule contacts-upload task, error \(err)") } } - 调整调度时机:仅在应用启动、任务执行完成后调度下一次,而非每次进入后台都触发。
- 提交前先取消同标识符的已有任务,避免重复调度:
2. uploadInBackground的实现是否合规?
当前实现存在多个不符合规范的点,会导致任务异常或优先级降低:
核心问题:
- 未响应任务取消:SwiftUI的
backgroundTask闭包未处理系统取消信号,若任务被强制终止却无响应,会影响后续任务调度优先级。 - 嵌套Task导致等待失效:
uploadInBackground中嵌套的Task会让外部await无法正确等待任务完成,可能任务仍在执行时,外层方法已结束,系统会误判任务完成并终止操作。 - 错误处理不完整:部分
guard直接return未抛出错误,调用方无法感知失败,导致系统误判任务成功。
修改后的合规实现:
static func uploadContactsBackgroundTask() async { guard store.permissionStatus == .authorized || store.permissionStatus == .restricted else { return } do { try await withTaskCancellationHandler { try await uploadInBackground() } onCancel: { // 取消正在进行的网络请求/文件操作 API.storage.cancelCurrentRequests() // 需确保API支持取消请求 } } catch { print("failed to finish uploading contacts: \(error)") } } static func uploadInBackground() async throws { // Fetching Contacts from local phonebook ... // Storing contacts locally ... // uploading to DB guard let user_id = UserDefaults.standard.string(forKey: "user_id") else { throw NSError(domain: "ContactsUpload", code: -1, userInfo: [NSLocalizedDescriptionKey: "用户ID未找到"]) } let filename = user_id + ".csv" guard let postData = FileManager.default.contents(atPath: fileURL.relativePath) else { throw NSError(domain: "ContactsUpload", code: -2, userInfo: [NSLocalizedDescriptionKey: "CSV文件不存在"]) } return try await withCheckedThrowingContinuation({ continuation in API.storage.createUrl(storagePath: "contacts", filename: filename) .onSuccess { createReq in let uploadTask = API.storage.uploadUrl(data: postData, filename: filename, upload_url: createReq.upload_url, mimeType: .csv) { res in switch res { case .success: continuation.resume() case .failure(let failure): continuation.resume(throwing: failure) } } // 监听任务取消信号,及时终止上传 Task { if await Task.isCancelled { uploadTask.cancel() // 需确保uploadUrl返回可取消的任务实例 continuation.resume(throwing: CancellationError()) } } } .onError({ err in continuation.resume(throwing: err) }) .onCancellation { continuation.resume(throwing: CancellationError()) } }) }
额外注意事项:
- 任务类型选择:通讯录上传属于非即时性网络任务,使用
BGProcessingTaskRequest比BGAppRefreshTaskRequest更合适,前者允许更长执行时间,系统会在充电、联网等合适时机调度。 - 权限配置:必须在
Info.plist中添加BGTaskSchedulerPermittedIdentifiers数组,包含任务标识符uploadContacts,否则任务无法被系统调度。 - 测试方法:使用Xcode的
Debug -> Simulate Background Refresh功能测试后台任务,真实环境中系统会根据应用活跃度调整调度频率,无法保证严格每2小时执行。
内容的提问来源于stack exchange,提问作者itamarg
相关产品推荐
相关产品推荐

