使用 @MainActor 更新 UI 的适当策略是什么?

Vla*_*kov 5 concurrency xcode ios async-await swift

假设您有一个在全局上下文中异步执行的方法。根据执行情况,您需要更新 UI。

private func fetchUser() async {
    do {
        let user = try await authService.fetchCurrentUser()
        view.setUser(user)
    } catch {
        if let error = error {
            view.showError(message: error.message)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

切换到主线程的正确位置在哪里?

  1. 分配@MainActorfetchUser()方法:
@MainActor
private func fetchUser() async { 
    ...
}
Run Code Online (Sandbox Code Playgroud)
  1. 分配@MainActorsetUser(_ user: User)showError(message: String)视图的方法:
class SomePresenter {

    private func fetchUser() async {
        do {
            let user = try await authService.fetchCurrentUser()
            await view.setUser(user)
        } catch {
            if let error = error {
                await view.showError(message: error.message)
            }
        }
    }

}

class SomeViewController: UIViewController {

    @MainActor
    func setUser(_ user: User) {
        ...
    }

    @MainActor
    func showError(message: String) {
        ...
    }

}
Run Code Online (Sandbox Code Playgroud)
  1. 不要分配@MainActor。使用await MainActor.runor Taskwith代替在主线程上@MainActor运行setUser(_ user: User)and (如):showError(message: String)DispatchQueue.main.async
private func fetchUser() async {
    do {
        let user = try await authService.fetchCurrentUser()
        
        await MainActor.run {
            view.setUser(user)
        }
    } catch {
        if let error = error {
            await MainActor.run {
                view.showError(message: error.message)
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Rob*_*Rob 3

选项 2 是合乎逻辑的,因为您让必须在主队列上运行的函数如此声明自己。然后,如果您错误地调用它们,编译器会警告您。更简单的是,您可以将具有这些函数的类声明为@MainActor, 本身,然后您不必如此声明各个函数。例如,因为设计良好的视图或视图控制器将自身限制为仅与视图相关的代码,所以将整个类声明为@MainActor并使用它来完成是安全的。

选项 3(代替选项 2)很脆弱,要求应用程序开发人员必须记住run在主要参与者上手动设置它们。如果你没有做正确的事情,你就会失去编译时警告。编译时警告总是好的。但 WWDC 2021 视频Swift concurrency: Update a example app指出,即使您采用选项 2,MainActor.run如果您需要调用一系列MainActor方法并且您可能不希望产生await一个又一个调用的开销,您可能仍然会使用,而是将主要参与者功能组包装在一个MainActor.run块中。(但您可能仍会考虑将其与选项 2 结合使用,而不是代替它。)

概括地说,选项 1 可以说有点严厉,它指定了一个不一定需要在主要参与者上运行的函数来执行此操作。您应该只在明确需要/期望的情况下使用主要演员。话虽如此,在实践中,我发现让演示者(或控制器或视图模型或您采用的任何模式)在主要参与者上运行通常也很有用。例如,如果您有同步UITableViewDataSourceUICollectionViewDataSource从演示者获取模型数据的方法,则尤其如此。如果相关的演示者使用不同的参与者,则无法始终同步返回数据源。因此,您也可以在主角上运行演示者方法。同样,最好将其与选项 2 结合起来考虑,而不是代替它。

所以,简而言之,选项2是谨慎的,但往往与选项1和3酌情结合。必须在主要参与者上运行的例程应该这样指定,而不是将这种负担强加给调用者。


前面提到的《Swift 并发:更新示例应用程序》涵盖了许多实际注意事项,如果您还没有看过,值得一看。