You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.02 07:45:29