跳到主要内容

避免 SwiftUI 任务阻塞 MainActor

· 阅读需 5 分钟
GoSwiftUI
goswiftui.com

我发现很容易不小心阻塞了 MainActor,并且因为一个我以为在后台线程上的长时间运行任务而导致用户界面挂起。这里有一个最近的例子。

用户界面挂起

Apple 建议不要在主线程上运行长期运行的任务。阻塞主线程超过100 毫秒,用户会注意到无响应或挂起的用户界面。

使用Swift 并发意味着将长期运行的工作保留在 MainActor 之外。这是一个我发现很容易犯的错误,尤其是在我开始在代码中散布 @MainActor 以禁止 严格并发检查 时。

看看这个带有按钮的 SwiftUI 视图,该按钮会增加计数:

@MainActor
struct ContentView: View {
@State private var appModel = AppModel()

var body: some View {
VStack {
Button("Count", action: appModel.count)
.buttonStyle(.borderedProminent)

Text(appModel.counter.formatted())
.fontDesign(.monospaced)
.font(.largeTitle)

Text("Running: \(appModel.running ? "Yes": "No")")
}
.task {
// MainActor 在这里
await appModel.start()
}
}
}

该视图有一个异步任务,在视图出现之前启动,当它完成时,它会更新下面显示的 running 状态。视图使用的两个方法和值是作为视图的状态属性声明的可观察应用程序模型的一部分。

AppModel 方法是从 MainActor 上的 ContentView 调用的,所以我最终将整个应用程序模型隔离到 MainActor

@MainActor
@Observable final class AppModel {
private(set) var counter: Int = 0
private(set) var running: Bool = false

func count() {
counter += 1
}

func start() async {
// MainActor 在这里
running = await doWork()
}

private func doWork() async -> Bool {
// 哪个 actor?
sleep(10)
return true
}
}

start 方法调用另一个异步方法(也在 AppModel 中声明),该方法执行长期运行的工作并返回状态。为了简洁起见,我用 10 秒的延迟替换了实际任务,该任务涉及导入一些数据。

也许你已经看出来了,但这会挂起用户界面 10 秒,直到 doWork 方法完成。在这种情况下,doWork 方法在 MainActor 上运行,因为整个 AppModel 都被隔离到 MainActor,我们最终阻塞了用户界面。

异步和非隔离

我发现 Apple 关于 提高应用程序响应速度 的这篇文章有助于理解问题和可能的解决方案。

引用要点:

使用 Swift 并发时,确保不要意外地在 MainActor 上执行工作。将繁重工作重构为非 actor 隔离的异步函数的正确方法取决于你是否可以这样做。

如果工作是异步的,它还需要从 MainActor 非隔离,如果你不想阻塞用户界面。

如果你能以这样的方式包装长期运行的工作,使其成为 asyncnonisolated,那么使用 Taskawait 在主 actor 之外执行它很容易。

如果这不可行,请在调用 Task.detached(priority:operation:) 方法时在同步函数内执行。

在我的情况下,可以将长期运行的工作与更新 AppModel 状态分开。这意味着我可以通过将 doWork 方法标记为 nonisolated 来解决我的用户界面挂起问题,这样它就不再在 MainActor 上运行:

private nonisolated func doWork() async -> Bool {
sleep(10)
return true
}

工具帮助?

对我来说,这种类型的问题的有趣之处在于它不是编译错误。启用 严格并发检查 在这里没有帮助。UI 挂起不是由数据竞争引起的,而是由在主线程上执行了太多工作引起的。这是无意的,但至少对我来说,这似乎是一个很容易犯的错误。

Instruments 严重挂起

你可以使用 Instruments 中的 Hangs 工具来检查 UI 挂起,但我希望看到 Xcode 帮助我们可视化源代码中发生的事情。例如,用不同的颜色向我展示 Main Actor 上和 Main Actor 外的代码,这样我就可以快速看到问题。