我遇到了一个问题,我的异步函数导致UI冻结,即使我在任务中调用它们,并尝试了许多其他的方法来做它(GCD,任务,分离和两者的组合)。测试后,我发现是哪个部分导致了这种行为,我认为这是Swift/SwiftUI中的一个bug。
错误描述
我想每隔x秒在后台计算一次,然后通过更新一个@Binding/@EnvironmentObject值来更新视图。为此我使用了一个计时器,并通过在.onReceive修饰符中订阅它来监听它的变化。这个修饰符的操作只是一个带有async函数的Task(await foo()).这和预期的一样,所以即使foo函数暂停几秒钟,UI也不会冻结但是如果我在视图中添加一个@EnvironmentObject,UI将在foo函数的持续时间内没有响应。
视图中没有EnvironmentVariable的行为的GIF:
视图中具有EnvironmentVariable的行为的GIF:
最小、可重现的示例
这只是一个按钮和一个滚动视图来查看动画。当你在代码中存在EnvironmentObject的情况下按下按钮时,UI会冻结并停止对手势的响应,但只要删除这一行,UI就可以正常工作,保持响应并更改属性。
import SwiftUI
class Config : ObservableObject{
@Published var color : Color = .blue
}
struct ContentView: View {
//Just by removing this, the UI freeze stops
@EnvironmentObject var config : Config
@State var c1 : Color = .blue
var body: some View {
ScrollView(showsIndicators: false) {
VStack {
HStack {
Button {
Task {
c1 = .red
await asyncWait()
c1 = .green
}
} label: {
Text("Task, async")
}
.foregroundColor(c1)
}
ForEach(0..<20) {x in
HStack {
Text("Placeholder \(x)")
Spacer()
}
.padding()
.border(.blue)
}
}
.padding()
}
}
func asyncWait() async{
let continueTime: Date = Calendar.current.date(byAdding: .second, value: 2, to: Date())!
while (Date() < continueTime) {}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
免责声明
我对这个项目所需的并发级别还很陌生,所以我可能遗漏了一些东西,但我找不到任何与搜索词“Task”和“EnvironmentObject”相关的东西。
问题
这真的是一个错误吗?还是我错过了什么?
2条答案
按热度按时间wkftcu5l1#
就我所知,你的代码,不管有没有
@EnvrionmentObject
,都应该 * 总是 * 阻塞主线程。事实上,没有@EnvironmentObject
它就不会阻塞,这可能是一个bug,但不是相反。在本例中,您阻塞了主线程--您调用了一个
async
函数,该函数在从其父级继承的上下文上运行。其父级是一个View
,它在主参与者上运行。通常在这种情况下,会对继承的上下文之外的东西的实际运行产生混淆。您提到使用
Task.detached
,但是只要您的函数在父级上仍然标记为async
,并且没有其他修改,in仍然会在主参与者上运行。为了避免继承父对象的上下文,您可以将其标记为
nonisolated
:或者,您可以将函数移到某个位置(例如移到
View
之外的ObservableObject
),该位置不会像View
那样显式地在主参与者上运行。请注意,这里还有一点欺骗性,因为您将函数标记为
async
,但它实际上并不执行任何异步工作--它只是阻塞了它所运行的上下文。qnyhuwrf2#
问题是
Task { ... }
会将任务添加到 current actor。如果您有一些缓慢、同步的任务,您永远不希望将其添加到主要actor。人们经常将Task { ... }
与DispatchQueue.global().async { ... }
混为一谈,但它们不是一回事。此外,您还应该避免在
@MainActor
隔离函数中放置任何缓慢和同步的内容。如果您希望从当前参与者中去除一些缓慢且同步的进程,通常可以使用
Task.detached { ... }
。或者,您可以为耗时的进程创建一个单独的参与者。但在这种情况下,不需要做任何这些,而是使用
Task.sleep
,它是sleep
的一个版本,专为Swift并发而设计,“不会阻塞底层线程”。避免旋转。
Thread.sleep
稍微好一点,但仍然不可取。使用Task.sleep
。