@MainActor — 它是什么、应用及在异步 Swift 代码中的特点

作者: IT Sectr 发布日期: 2026-03-19 阅读时间: 8 分钟

@MainActor 是 Swift 语言中的一个全局演员,确保代码在主线程上执行。根据 Apple Developer, 2024,@MainActor 在处理 UI 时自动化切换到主线程,让开发者免去手动调用 DispatchQueue.main.async。该注解随着 Swift 5.5 一起出现,同时引入了 async/await 系统。

主要观点

  • @MainActor — Swift 全局演员,确保在主线程上执行。
  • async/await 系统 — @MainActor 工作的基础。
  • 类的注解 自动将其所有方法放置在主线程上。
  • 与 DispatchQueue.main 不同,@MainActor 在编译器层面检查线程。
  • UI 更新 — @MainActor 在 iOS 开发中的主要应用领域。

什么是 @MainActor?

@MainActor 是 Swift 中的一个全局演员(global actor),它结合了演员的特性和在应用程序主线程上执行的保证。它是 Swift 并发系统的一部分,随着 Swift 5.5 与 async/await 和结构化并发一起引入。该注解让开发者不必考虑手动切换线程,并减少了 UI 错误的数量。

定义及在 Swift Concurrency 中的地位

Swift 中的演员是一种引用类型,它封装其状态并保证只有一个线程可以修改它。@MainActor 是一个特殊的 全局演员,其执行者是主线程。任何标记为 @MainActor 的代码都在主线程上执行 — 即使它是从后台任务中调用的。

在 @MainActor 出现之前,开发者通过 DispatchQueue.main.async 手动切换到主线程。这是常见错误的来源:开发者忘记切换,导致因在非主线程上更新 UI 而崩溃。@MainActor 在类型系统层面解决了这个问题。

创建原因

iOS 应用程序中多数错误的来源是 UI 不安全 — 从后台线程更新界面。Apple 将 @MainActor 嵌入 Swift Concurrency,使得切换到主线程变得自动并可由编译器检查,消除了整个类别的运行时错误。

@MainActor 如何工作?

工作原理 @MainActor 基于 Swift Concurrency 的执行系统。当一个线程调用标记为 @MainActor 的函数时,调度器会在当前执行者上挂起它,并在主线程上恢复它。编译器跟踪调用边界并保证安全。

主线程执行者

负责执行 @MainActor 的是 MainActor.shared — 与应用程序主线程关联的执行者。当异步函数被标记为 @MainActor 时,它始终在此执行者上恢复,无论原始任务在哪个线程上启动。

swift
import SwiftUI

class ViewModel: ObservableObject {
    @Published var items: [String] = []

    @MainActor
    func loadData() async {
        let result = await fetchRemoteData()
        items = result  // 安全,MainActor 保证主线程
    }
}

演员上下文的继承

如果一个函数被标记为 @MainActor 并调用另一个异步函数,它默认继承 演员上下文。这意味着所有嵌套调用也在主线程上执行,除非另有规定。编译器跟踪这一点,并在尝试传递不兼容的闭包时报错。

@MainActor 与 DispatchQueue.main

比较 @MainActor 和 DispatchQueue.main 有助于理解为什么新机制被认为更安全和便捷,尽管两者都解决同一个任务 — 在主线程上执行代码。

类型层面的安全

@MainActor 是 在编译器层面进行的检查。如果你尝试从不安全的上下文调用 @MainActor 函数,编译器会发出警告或报错。DispatchQueue.main.async 是一个运行时调用:代码可以编译,但在尝试从后台线程更新 UI 时可能在运行时崩溃。

性能和冗余

DispatchQueue.main.async 向队列添加一个可能延迟执行的块。@MainActor 使用 async/await 进行 直接执行者切换,无需创建额外的闭包。这降低了开销,并使代码在执行时间方面更可预测。

swift
// 旧方法
DispatchQueue.main.async {
    self.updateUI()
}

// 使用 @MainActor 的新方法
@MainActor
func updateUI() {
    // 在主线程上执行
    self.label.text = "已更新"
}
标准@MainActorDispatchQueue.main
检查编译器运行时
语法注解(声明式)调用(命令式)
开销低(执行者切换)中等(闭包 + 队列)
可测试性高(MainActor.shared 可以替换)低(难以 Mock)

在 iOS 项目中使用 @MainActor

在实际的 iOS 项目中,@MainActor 应用于 ViewModel 层、SwiftUI 视图和 UIKit 控制器。注解可以应用于单个方法,也可以应用于整个类型。

类或结构的注解

通过将类标记为 @MainActor,你保证该类的 所有方法和属性 仅在主线程上可访问。这对于 SwiftUI 视图和 ObservableObject 类特别方便:只需在 class 前添加 @MainActor,所有 @Published 属性就会安全地更新。

swift
@MainActor
final class UserListViewModel: ObservableObject {
    @Published var users: [User] = []
    @Published var isLoading = false

    func fetchUsers() async {
        isLoading = true
        users = await api.getUsers()
        isLoading = false
    }
}

封装旧代码

在处理旧的 UIKit 代码时,其中线程切换是手动的,可以使用 MainActor.run 进行显式切换。这对于逐步迁移到 Swift Concurrency 而无需重写整个代码基础很方便。

swift
await MainActor.run {
    self.tableView.reloadData()
}

@MainActor 的局限性

尽管有诸多优点,@MainActor 也有一系列局限性,在设计应用程序架构时必须考虑。理解应用边界有助于避免错误使用。

高强度使用时的性能

如果整个调用链都被标记为 @MainActor,任何重任务 都将在主线程上执行,导致 UI 冻结。建议仅将 UI 层标记为 @MainActor,而将业务逻辑和网络请求留在后台演员或全局执行者中。

与某些 API 的不兼容

基于回调的旧 API(例如没有 async/await 的 URLSession)不支持演员上下文。集成需要使用 CheckedContinuation 进行封装。此外,@MainActor 不兼容 performSelector、target-action 和其他非异步的 UIKit 模式。

多线程调试

在调试使用 @MainActor 的应用程序时,更难以复现 竞争条件,因为编译器在构建阶段而非运行时阻止了其中的许多个。但这可能创造虚假的安全感:与共享可变对象(例如 NSCache 或共享全局变量)的不正确操作仍然可能,如果它们没有被标记为 @MainActor 并且没有显式同步就被使用。

测试 @MainActor

@MainActor 大大简化了 UI 逻辑的测试,因为它消除了在测试中手动切换线程的需要。但是,在编写单元测试和 UI 测试时有一些特点需要考虑。

使用 MainActor 的单元测试

在 XCTest 中,执行环境自动配置 主线程执行者。当测试方法在主线程上执行时,调用 @MainActor 函数无需额外配置 — 它们在相同的上下文中执行。要测试后台场景,请在 Task 内使用 MainActor.run,显式指定优先级和执行者,并单独检查代码在从后台调用时是否正确工作。

常见方法之一是测试 使用 @MainActor 的 ViewModel,检查 @Published 属性在异步操作后是否正确更新。由于演员上下文的继承,测试内的 await 调用确保在主线程上执行,无需额外的 DispatchQueue 保证和手动上下文切换,这简化了测试的编写。

重构时检查封装性

在将现有代码重构为 Swift Concurrency 时,通过编译器检查 @MainActor 封装性:从 @MainActor 上下文中调用任何没有 @MainActor 的同步方法都会被标记为错误。这个特性用于将项目逐步迁移到 async/await:将 ViewModel 层标记为 @MainActor,编译器将标记所有需要移动到后台演员的不安全调用。

Mock 和演员上下文

在为 @MainActor 依赖创建 mock 时,使用 具有 async 方法的协议,这些协议声明带有返回类型的异步函数。这允许替换网络服务、数据库和其他外部依赖,而不破坏演员封装。编译器将检查 mock 是否满足所有封装要求,防止从后台测试线程意外访问 @MainActor 代码。

等待异步操作

在同步测试 @MainActor 代码时,使用 XCTestExpectation 等待异步操作完成。在测试中设置期望,并在主线程上执行的闭包内执行 fulfillment。如果测试无限挂起 — 可能没有在主线程上发生调用,需要检查演员封装。要调试执行上下文,在测试代码中添加 Thread.isMainThread 检查是有用的。

常见问题

是否必须将整个类标记为 @MainActor?

不,只需 标记更新 UI 的方法就够了。但如果类中有多个这样的方法,将 @MainActor 添加到整个类更简单。这保证其所有成员在主线程上执行,并简化代码维护。

@MainActor 与 @globalActor 有什么区别?

@MainActor 是全局演员的一个具体实例,绑定到主线程。@globalActor 是一个协议,用于创建自己的全局演员。例如,如果项目架构要求,你可以创建 @BackgroundActor 在后台线程上执行代码。

可以不使用 async/await 而使用 @MainActor 吗?

可以,带有 @MainActor 的 同步函数 也在主线程上执行。但 @MainActor 的主要价值在于与 async/await 结合使用,异步函数自动在主线程上恢复,无需通过 DispatchQueue.main 手动切换。

如何取消 @MainActor 任务?

Task.cancel() 对 @MainActor 任务的作用与普通任务相同。@MainActor 任务可以检查 Task.isCancelled 或抛出 CancellationError。取消时主线程不会被阻塞 — 任务仅在最近的挂起点停止执行。

如果从后台线程调用 @MainActor 会怎样?

编译器 保证安全:如果从后台上下文调用 @MainActor 函数,编译器会指出错误。对于异步调用,只需将调用代码标记为 await,执行者会自动切换到主线程。对于同步调用,需要通过 MainActor.run 进行显式切换。

总结

  • @MainActor — Swift 全局演员,确保在主线程上执行。
  • 编译器检查 消除了整个类别的 UI 安全错误。
  • 整个类的注解 自动将其所有方法放置在主线程上。
  • MainActor.run — 针对旧代码和同步上下文的显式切换。
  • 与 DispatchQueue.main 不同,@MainActor 不创建闭包,而是使用执行者切换。
  • 重型计算 不应在 @MainActor 下执行,以避免 UI 冻结。
  • 上下文继承 简化了异步调用链,使代码顺序、可预测并对 UI 安全。

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读