Swift异步函数后台运行失败致UI卡顿问题求助
我尝试让异步函数在后台线程运行以避免阻塞主线程,但遇到了问题。我把一个耗时约5秒的方法标记为async,调用时用await,但UI还是会冻结,达不到预期效果。
编辑1
因为Swift 5.5的并发机制可以替代DispatchQueue,所以希望仅通过Async/Await解决这个问题。
编辑2
尝试移除@MainActor包装器后,函数似乎还是在主线程运行。
NumberManager.swift
@MainActor class NumberManager: ObservableObject { @Published var numbers: [Double]? func generateNumbers() async { var numbers = [Double]() numbers = (1...10_000_000).map { _ in Double.random(in: -10...10) } self.numbers = numbers // 运行耗时约5秒... } }
ContentView.swift
struct ContentView: View { @StateObject private var numberManager = NumberManager() var body: some View{ TabView{ VStack{ DetailView(text: isNumbersValid ? "First number is: \(numberManager.numbers![0])" : nil) .onAppear() { Task { // 在主线程运行,UI会冻结直到任务完成 await numberManager.generateNumbers() } } } .tabItem { Label("One", systemImage: "list.dash") } Text("Hello") .tabItem { Label("Two", systemImage: "square.and.pencil") } } } var isNumbersValid: Bool{ numberManager.numbers != nil && numberManager.numbers?.count != 0 } }
已尝试方案
我试了几种方法,唯一能让函数在后台运行的方式是修改成下面这样,但我知道应该尽量避免用Task.detached,觉得这不是正确的解决方式。
func generateNumbers() async { Task.detached { var numbers = [Double]() numbers = (1...10_000_000).map { _ in Double.random(in: -10...10) } await MainActor.run { [numbers] in self.numbers = numbers } } }
核心原因
你的NumberManager被@MainActor修饰,导致整个类的方法默认都在主线程执行,哪怕标记了async也不会自动切换到后台。即使移除@MainActor,调用时的Task是在onAppear中创建的——而onAppear本身运行在主线程,这个Task会继承主线程的执行上下文,所以耗时任务还是会阻塞UI。
正确实现方式
不需要用Task.detached,下面两种方式既能保证耗时任务在后台执行,又符合Swift Async/Await的最佳实践:
方法1:用nonisolated分离耗时逻辑
将生成随机数的耗时逻辑封装到一个非隔离函数中,让它自动在后台线程执行,最后回到主线程更新UI:
@MainActor class NumberManager: ObservableObject { @Published var numbers: [Double]? func generateNumbers() async { // 调用非隔离函数,自动在后台执行耗时操作 let newNumbers = await generateRandomNumbers() // 类在MainActor上下文,这里自动切回主线程更新属性 self.numbers = newNumbers } // 标记为nonisolated,脱离MainActor上下文 nonisolated private func generateRandomNumbers() async -> [Double] { return (1...10_000_000).map { _ in Double.random(in: -10...10) } } }
方法2:在异步函数内显式切换上下文
如果不想拆分函数,也可以在generateNumbers里用Task指定后台执行耗时操作:
@MainActor class NumberManager: ObservableObject { @Published var numbers: [Double]? func generateNumbers() async { // 创建后台任务执行耗时逻辑 let newNumbers = await Task { @Sendable in return (1...10_000_000).map { _ in Double.random(in: -10...10) } }.value // 自动回到主线程更新UI self.numbers = newNumbers } }
为什么不用Task.detached?
Task.detached会创建完全脱离当前上下文的任务,虽然能在后台运行,但需要手动调用MainActor.run更新UI,代码冗余且容易出错。上面两种方式利用Swift并发的上下文自动切换机制,代码更简洁安全。
内容的提问来源于stack exchange,提问作者Lucas Chae

