Swift iOS后台刷新时HTTP POST请求令牌无响应问题求助
iOS后台刷新时POST请求刷新令牌无响应问题排查
本人熟悉C#及Xamarin开发,刚接触Swift、SwiftUI和原生iOS开发。当前用Xcode14+SwiftUI开发带Watch扩展的iOS应用,前台所有流程(登录获取OAuth2令牌、GET请求数据、同步至Watch)正常,后台刷新时GET请求也能正常执行,但令牌过期后,后台用POST请求通过refresh_token刷新令牌时,URLSession代理方法完全不触发,必须将APP切到前台才会执行相关逻辑。
相关代码片段
入口代码
@main struct MyApp: App { @StateObject private var viewModel = AppViewModel() @Environment(\.scenePhase) private var scenePhase var body: some Scene { WindowGroup { if !self.viewModel.initialized { StartupView() } else if !self.viewModel.loggedIn { LoginView(appViewModel: self.viewModel) } else { MainView(appViewModel: self.viewModel) } } .onChange(of: scenePhase) { newPhase in if newPhase == .background { viewModel.scheduleBackgroundRefresh() } } .backgroundTask(.appRefresh(AppViewModel.BG_REFRESH_IDENTIFIER)) { await viewModel.performBackgroundRefresh() } } }
后台刷新调度代码
func scheduleBackgroundRefresh() { let request = BGAppRefreshTaskRequest(identifier: AppViewModel.BG_REFRESH_IDENTIFIER) request.earliestBeginDate = .now.addingTimeInterval(5.0 * 60.0) do { try BGTaskScheduler.shared.submit(request) print("Background-Task scheduled!") // e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"<MyIdentifier>"] } catch let ex { print("Background-Task schedule error:\n\(ex.localizedDescription)") } }
后台刷新执行代码
func performBackgroundRefresh() async { if self.vehicleToUseForBackgroundRefresh == nil { return } var newCarState: CarStateModel? = nil var occuredEx: Error? = nil do { if !(self.psa.accessToken?.expired ?? true) { // Fetch data with HTTP GET, only if the token hasn't expired. newCarState = try AppViewModel.loadCarState(for: self.vehicleToUseForBackgroundRefresh!, with: self.psa, callFromBackgroundRefresh: true) AppViewModel.transferCarStateToWatch(newCarState!, via: self.connectivityManager) } } catch let ex { newCarState = nil occuredEx = ex } // Always refresh Token asynchronously when running background thread. // You never know when iOS schedules the refresh, so always refresh if you have the possibility! // ### AND HERE THE PROBLEM BEGINS! ### try? self.psa.refreshToken(fromBackgroundThread: true) if newCarState != nil || occuredEx != nil { DispatchQueue.background(task: { self.delegate?.newCarStateFromBackgroundRefresh(newCarState, error: occuredEx) }) } }
令牌刷新代码
func refreshToken(fromBackgroundThread: Bool = false) throws { if self.accessToken == nil { throw PSAErrorModel(code: -1, debug: "refreshToken", message: "Not signed in.") } let parameters: [String: Any] = [ "grant_type": "refresh_token", "refresh_token": self.accessToken!.refresh_token ] if fromBackgroundThread { // This is called when refreshing the token from app background. self.acquireTokenAsync(parameters: parameters) } else { // This is a "synchronous" call, using DispatchSemaphore. // But this only fires when the app is active and it works great. self.accessToken = try self.acquireToken(parameters: parameters) } }
异步获取令牌代码
private func acquireTokenAsync(parameters: [String : Any]) { let urlSession = BackgroundURLRequest() urlSession.delegate = self // Same URLRequest as in foreground refresh, so thats not different. let request = self.createUrlRequestForTokenAcquiry(parameters: parameters) urlSession.startDataTask(with: request) }
问题核心代码
func startDataTask(with request: URLRequest) { // Until here every breakpoint gets hit. // But when breaking here and pressing "resume", none of the delegate functions // are fired UNTIL I BRING THE APP TO FRONT. Without bringing the app to front, // the delegate functions will NEVER fire. And that's my problem. session.dataTask(with: request).resume() }
BackgroundURLRequest实现
// These delegates are implemented in my API-Class. So when the request finishes, the received OAuth-Token will be stored. protocol BackgroundURLRequestDelegate: AnyObject { func backgroundUrlRequestFinished(session: URLSession, dataTask: URLSessionDataTask, receivedData: Data) func backgroundUrlRequestFailed(session: URLSession, task: URLSessionTask, error: Error?) } class BackgroundURLRequest: NSObject { static let shared = BackgroundURLRequest() private static let identifier = "<MyIdentifier>" weak var delegate: BackgroundURLRequestDelegate? private var session: URLSession! var savedCompletionHandler: (() -> Void)? override init() { super.init() let configuration = URLSessionConfiguration.background(withIdentifier: BackgroundURLRequest.identifier) configuration.sessionSendsLaunchEvents = true configuration.isDiscretionary = false configuration.allowsCellularAccess = true session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil) } func startDataTask(with request: URLRequest) { // Until here every breakpoint gets hit. // But when breaking here and pressing "resume", none of the delegate functions // are fired UNTIL I BRING THE APP TO FRONT. Without bringing the app to front, // the delegate functions will NEVER fire. And that's my problem. session.dataTask(with: request).resume() } } extension BackgroundURLRequest: URLSessionDelegate { func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) { DispatchQueue.main.async { self.savedCompletionHandler?() self.savedCompletionHandler = nil } } } extension BackgroundURLRequest: URLSessionTaskDelegate { func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { print("BackgroundURLRequest ERROR:\n\(error?.localizedDescription ?? "No description.")") self.delegate?.backgroundUrlRequestFailed(session: session, task: task, error: error) } } extension BackgroundURLRequest: URLSessionDataDelegate { func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { self.delegate?.backgroundUrlRequestFinished(session: session, dataTask: dataTask, receivedData: data) } }
问题排查与解决方案
1. 核心问题:局部BackgroundURLRequest实例被提前释放
acquireTokenAsync中每次创建新的BackgroundURLRequest实例,方法执行完毕后该实例会被ARC回收,而delegate是弱引用,此时代理对象变为nil,导致URLSession的回调无法触发。
修复方案:使用已定义的单例BackgroundURLRequest.shared,避免实例被回收:
private func acquireTokenAsync(parameters: [String : Any]) { let urlSession = BackgroundURLRequest.shared urlSession.delegate = self let request = self.createUrlRequestForTokenAcquiry(parameters: parameters) urlSession.startDataTask(with: request) }
2. 后台任务未等待异步请求完成
performBackgroundRefresh是异步方法,但调用refreshToken后未等待异步令牌刷新操作完成,后台任务就直接结束,系统会终止APP进程,导致请求无法执行完成。
修复方案:将refreshToken改为async方法,使用async/await等待操作完成:
// 修改refreshToken为async方法 func refreshToken(fromBackgroundThread: Bool = false) async throws { if self.accessToken == nil { throw PSAErrorModel(code: -1, debug: "refreshToken", message: "Not signed in.") } let parameters: [String: Any] = [ "grant_type": "refresh_token", "refresh_token": self.accessToken!.refresh_token ] if fromBackgroundThread { try await self.acquireTokenAsync(parameters: parameters) } else { self.accessToken = try self.acquireToken(parameters: parameters) } } // 修改acquireTokenAsync为支持async/await的方法 private func acquireTokenAsync(parameters: [String : Any]) async throws { let urlSession = BackgroundURLRequest.shared let request = self.createUrlRequestForTokenAcquiry(parameters: parameters) return try await withCheckedThrowingContinuation { continuation in // 临时保存原代理,避免覆盖 let originalDelegate = urlSession.delegate urlSession.delegate = self // 扩展BackgroundURLRequest,添加任务与continuation的映射 // 在BackgroundURLRequest类中添加:private var taskContinuationMap = [Int: CheckedContinuation<Data, Error>]() let task = urlSession.session.dataTask(with: request) (urlSession as? BackgroundURLRequest)?.taskContinuationMap[task.taskIdentifier] = continuation task.resume() // 在BackgroundURLRequest的代理方法中触发continuation // 修改URLSessionDataDelegate方法: /* func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { if let continuation = taskContinuationMap.removeValue(forKey: dataTask.taskIdentifier) { continuation.resume(returning: data) } delegate?.backgroundUrlRequestFinished(session: session, dataTask: dataTask, receivedData: data) } */ // 修改URLSessionTaskDelegate方法: /* func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { if let continuation = taskContinuationMap.removeValue(forKey: task.taskIdentifier) { if let error = error { continuation.resume(throwing: error) } else { continuation.resume(returning: Data()) } } delegate?.backgroundUrlRequestFailed(session: session, task: task, error: error) } */ } } // 在performBackgroundRefresh中等待刷新完成 func performBackgroundRefresh() async { // ... 原有代码 // 等待令牌刷新完成 do { await self.psa.refreshToken(fromBackgroundThread: true) } catch let ex { print("Token refresh failed in background: \(ex.localizedDescription)") } // ... 原有代码 }
3. 后台URLSession回调处理完善
在SwiftUI的App结构体中添加后台URLSession唤醒事件处理,确保savedCompletionHandler被正确调用:
@main struct MyApp: App { @StateObject private var viewModel = AppViewModel() @Environment(\.scenePhase) private var scenePhase var body: some Scene { WindowGroup { // ... 原有视图代码 } .onChange(of: scenePhase) { newPhase in if newPhase == .background { viewModel.scheduleBackgroundRefresh() } } .backgroundTask(.appRefresh(AppViewModel.BG_REFRESH_IDENTIFIER)) { await viewModel.performBackgroundRefresh() } // 处理后台URLSession唤醒 .onOpenURL { _ in BackgroundURLRequest.shared.savedCompletionHandler?() } } }
内容的提问来源于stack exchange,提问作者MightyMike
相关产品推荐
相关产品推荐

