Swift中使用Task导致App冻结的问题排查与解决
问题:网络恢复后App主线程无响应(从DispatchQueue切换到Task后出现)
背景
我开发了一款复杂应用,断网时会清除现有数据并等待网络恢复;网络恢复后,重新启动WebSocket连接、订单校验等功能。之前用DispatchQueue.main.async { }时运行正常,但从applicationDidFinishLaunching开始改用Task(priority: .userInitiated, operation: {})调用异步函数后,出现异常。
问题现象
App会记录所有事件日志(类似Firebase控制台),但断网再重连后,logsVC失去主线程响应且无报错信息。网络重连是通过识别WKWebSocket因网络故障断开触发的。
相关代码
WebSocket失败处理函数
func webSocket(_ webSocket: WebSocket, didFailedWith error: Error) { let error = error as NSError switch error.code { case 401: // 认证失败 Task(priority: .userInitiated) { logprint("刷新 \(webSocket.name) token 以重新连接....") do { try await self.refreshToken() if try await self.connectWebSocket() == false { log("文件\(#file)第\(#line)行出现错误", .failure) } } catch { log("\(webSocket.name) 重新连接WebSocket失败...", error) } } case 101: // 连接被对等端重置 Task(priority: .userInitiated) { await webSocket.networkFailed() } default: log("\(webSocket.name) 连接被拒绝...", error) } } func networkFailed() async { self.isConnected = false await App.shared.reloadWhenInternetAvailable() }
App网络恢复重载函数
func reloadWhenInternetAvailable() async { log("等待网络恢复后重载.... \(self.isNetworkAvailable)", .info) guard self.isNetworkAvailable else { return } let t1 = Time.now guard self.appLaunchedAt.needsWebSocketConnection || t1.needsWebSocketConnection else { return } Calendar.default.cancelTimer() self.isNetworkAvailable = false let t = t1 - self.appLaunchedAt log("网络已断开,断开时长 \(t.conventionLabel)", .warning) await MainActor.run { self.delegate.status = .internetNotAvailable UserNotifications.error("网络已断开...").fire(.internet) } await Reachablity.shared.waitUntilInternetAvailable() let t2 = Time.now - t1 // 有时该日志不打印:网络恢复耗时约2小时时不打印,耗时较短时会在GUI中显示 log("网络已恢复,恢复耗时 \(t2.conventionLabel)", .success) await MainActor.run { UserNotifications.ID.internet.revoke() UserNotifications.success("网络已重新连接!").fire(.temprory) } await delay(2) await self.relaunch() }
Reachability相关函数
public func waitUntilInternetAvailable() async { if self.isNetworkLoopRunning { logprint("检查循环已启动... \(Time.now.debugDescription)") while !self.isConnectedToNetwork { await delay(milli: 1) } logprint("检查循环已结束... \(Time.now.debugDescription)") } else { await self.checkNetworkLoop() } } private func checkNetworkLoop() async { logprint("等待网络恢复... \(Time.now.debugDescription)") self.isNetworkLoopRunning = true while await !self.isAvailable() { await delay(3) } self.isNetworkLoopRunning = false } public func isAvailable() async -> Bool { do { var req = URLRequest(url: self.url) req.httpMethod = "GET" req.timeoutInterval = 3.0 req.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData _ = try await URLSession.shared.data(for: req) await MainActor.run { self.isConnectedToNetwork = true } return true } catch { let error = error as NSError if !self.isNetworkLostError(error) { let error = error as NSError log("网络检查错误代码无法识别.", error) } await MainActor.run { self.isConnectedToNetwork = false } return false } }
Delay函数
func delay(_ seconds: UInt64) async { do { try await Task.sleep(nanoseconds: seconds * 1_000_000_000) } catch { print("错误:等待\(seconds)秒失败,错误信息 \(error.localizedDescription)") } } // 0到10;5代表500毫秒 func delay(milli seconds: UInt64) async { do { try await Task.sleep(nanoseconds: seconds * 100_000_000) } catch { print("错误:等待0.\(seconds)秒失败,错误信息 \(error.localizedDescription)") } }
解决方案
1. 修复Task优先级与主线程调度冲突
从applicationDidFinishLaunching启动的Task(priority: .userInitiated)可能抢占主线程资源,导致UI组件无法及时更新。建议:
- 启动异步任务时,明确区分主线程与后台逻辑,避免后台Task阻塞主线程:
Task(priority: .userInitiated) { await MainActor.run { // 仅放需要主线程执行的初始化操作 } // 后续异步逻辑放在MainActor外执行 }
2. 优化Reachability的循环等待逻辑
waitUntilInternetAvailable中的无限轮询会长期占用线程资源,尤其是网络恢复耗时较长时,会阻塞后续任务调度。建议:
- 用通知或
AsyncStream替代轮询,监听系统网络状态变化,避免空循环:public func waitUntilInternetAvailable() async { for await isAvailable in networkStatusStream { if isAvailable { break } await delay(3) } } - 或为轮询添加取消检查,避免无意义的资源占用:
while await !self.isAvailable() { try Task.checkCancellation() // 检查当前Task是否被取消 await delay(3) }
3. 确保UI更新严格在MainActor执行
虽然代码中用到了await MainActor.run,但需确认所有涉及logsVC的日志更新操作都明确绑定主线程。例如,将日志函数标记为@MainActor:
@MainActor func log(_ message: String, _ level: LogLevel) { // 更新logsVC的UI逻辑 }
4. 排查Task取消与生命周期问题
网络恢复后调用的relaunch()可能启动新Task,但旧Task未被正确取消,导致线程资源耗尽。建议:
- 在
reloadWhenInternetAvailable开始时,取消之前的相关Task:private var reloadTask: Task<Void, Never>? func reloadWhenInternetAvailable() async { reloadTask?.cancel() reloadTask = Task { // 原有逻辑 } await reloadTask?.value }
内容的提问来源于stack exchange,提问作者Youmate
相关产品推荐
相关产品推荐

