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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 14:52:33