swift 在视图中有一个@EnvironmentObject会导致在主线程上执行任务

xoefb8l8  于 2022-10-31  发布在  Swift
关注(0)|答案(2)|浏览(194)

我遇到了一个问题,我的异步函数导致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”相关的东西。

问题

这真的是一个错误吗?还是我错过了什么?

wkftcu5l

wkftcu5l1#

就我所知,你的代码,不管有没有@EnvrionmentObject,都应该 * 总是 * 阻塞主线程。事实上,没有@EnvironmentObject它就不会阻塞,这可能是一个bug,但不是相反。
在本例中,您阻塞了主线程--您调用了一个async函数,该函数在从其父级继承的上下文上运行。其父级是一个View,它在主参与者上运行。
通常在这种情况下,会对继承的上下文之外的东西的实际运行产生混淆。您提到使用Task.detached,但是只要您的函数在父级上仍然标记为async,并且没有其他修改,in仍然会在主参与者上运行。
为了避免继承父对象的上下文,您可以将其标记为nonisolated

nonisolated func asyncWait() async {
    let continueTime: Date = Calendar.current.date(byAdding: .second, value: 2, to: Date())!
    while (Date() < continueTime) {}
}

或者,您可以将函数移到某个位置(例如移到View之外的ObservableObject),该位置不会像View那样显式地在主参与者上运行。
请注意,这里还有一点欺骗性,因为您将函数标记为async,但它实际上并不执行任何异步工作--它只是阻塞了它所运行的上下文。

qnyhuwrf

qnyhuwrf2#

问题是Task { ... }会将任务添加到 current actor。如果您有一些缓慢、同步的任务,您永远不希望将其添加到主要actor。人们经常将Task { ... }DispatchQueue.global().async { ... }混为一谈,但它们不是一回事。
此外,您还应该避免在@MainActor隔离函数中放置任何缓慢和同步的内容。
如果您希望从当前参与者中去除一些缓慢且同步的进程,通常可以使用Task.detached { ... }。或者,您可以为耗时的进程创建一个单独的参与者。
但在这种情况下,不需要做任何这些,而是使用Task.sleep,它是sleep的一个版本,专为Swift并发而设计,“不会阻塞底层线程”。

Button {
    Task {
        c1 = .red
        try await Task.sleep(nanoseconds: 2 * NSEC_PER_SEC)
        c1 = .green
    }
} label: {
    Text("Task, async")
}
.foregroundColor(c1)

避免旋转。Thread.sleep稍微好一点,但仍然不可取。使用Task.sleep

相关问题