WWDC Quick Look 💓 By SwiftGGTeam
Embracing Swift concurrency

Embracing Swift concurrency

观看原视频

Highlight

Xcode 26 新建 App 项目默认开启 Main Actor 模式,所有代码隐式带上 @MainActor,需要后台执行时再用 @concurrent 显式标注。


核心内容

很多开发者第一次写网络请求的时候都踩过同一个坑:在主线程同步调 URLSession.shared.data(from:),UI 立刻卡死几秒,应用看起来像挂掉了。这是因为主线程既要响应触摸事件、又要刷新 UI,一旦被网络 IO 占住就什么都干不了。Swift 团队这次给出的方案不再是新增一套 GCD API,而是从语言层面把”在哪个线程跑”这件事变成可被编译器检查的属性。

iOS 19 / Xcode 26 的新 App 项目默认开启 Main Actor 模式。这意味着你不写任何标注的代码都隐式带着 @MainActor,编译器知道它只能在主线程跑,于是允许你随便访问全局变量、单例。当某段计算(比如图片解码)开始让 UI 掉帧,你给那个函数加上 @concurrent,编译器会立刻指出它访问了哪些主线程独占的状态,逼你把这些状态用值类型或 actor 包好再传过去。整个迁移路径是渐进的:从单线程开始,先用 async/await 处理高延迟,再用 @concurrent 卸载 CPU 密集任务,最后用 actor 把数据本身搬离主线程。


详细内容

整段演讲围绕一个图片加载的例子展开。最初版本是同步阻塞的:

final class ImageModel {
  var imageCache: [URL: Image] = [:]
  let view = View()

  func fetchAndDisplayImage(url: URL) throws {
    let data = try Data(contentsOf: url)
    let image = decodeImage(data)
    view.displayImage(image)
  }
}

关键点:

  • fetchAndDisplayImage 是同步函数,整个流程在主线程一口气跑完。
  • 本地读小文件没问题,但如果换成网络 URL,主线程会被网络 IO 阻塞,UI 直接冻住。

第一步是把它改成 async,让网络 IO 不再阻塞主线程(06:10):

final class ImageModel {
  var imageCache: [URL: Image] = [:]
  let view = View()

  func fetchAndDisplayImage(url: URL) async throws {
    let (data, _) = try await URLSession.shared.data(from: url)
    let image = decodeImage(data)
    view.displayImage(image)
  }
}

关键点:

  • 函数签名加了 async,调用 URLSession 时用 await
  • await 标记了挂起点,函数会在这里让出主线程,等数据回来再恢复。
  • 函数还在主线程上,但主线程在等网络期间可以处理触摸事件、绘制其他 UI。
  • URLSession 的实现在后台线程做实际 IO,应用代码本身没引入并发。

async 函数必须在 task 里启动。点击按钮时这样写(07:31):

func onTapEvent() {
  Task {
    do {
      try await fetchAndDisplayImage(url: url)
    } catch let error {
      displayError(error)
    }
  }
}

关键点:

  • Task { ... } 创建一个新任务,包裹整个 fetch + display 流程。
  • 任务从头到尾按顺序执行,多个任务之间通过 interleaving 在主线程上轮流跑。
  • error 在 task 内部 catch,避免吞掉异常。

当应用真的遇到主线程卡顿——比如 decodeImage 处理大图太慢——才需要把这部分搬到后台。这时用 @concurrent11:11):

final class ImageModel {
  var imageCache: [URL: Image] = [:]
  let view = View()

  func fetchAndDisplayImage(url: URL) async throws {
    let (data, _) = try await URLSession.shared.data(from: url)
    let image = await decodeImage(data, at: url)
    view.displayImage(image)
  }

  @concurrent
  func decodeImage(_ data: Data, at url: URL) async -> Image {
    Image()
  }
}

关键点:

  • @concurrent 告诉 Swift 这个函数总是在后台运行。
  • 调用方用 await 等结果,主线程在解码期间可以继续干别的。
  • 此时 decodeImage 不再处于 main actor 隔离域,编译器会拒绝它直接访问 imageCache——这是设计目的,强制你把跨线程共享的数据显式处理。

针对共享可变引用类型的数据竞争问题,演讲给出 actor 方案(25:10):

actor NetworkManager {
  var openConnections: [URL: Connection] = [:]

  func openConnection(for url: URL) async -> Connection {
    if let connection = openConnections[url] {
      return connection
    }
    let connection = Connection()
    openConnections[url] = connection
    return connection
  }

  func closeConnection(_ connection: Connection, for url: URL) async {
    openConnections.removeValue(forKey: url)
  }
}

关键点:

  • class 改成 actoropenConnections 的可变状态被 actor 隔离保护。
  • 外部调用 openConnection / closeConnection 必须 await,actor 保证同一时刻只有一个调用在内部执行。
  • 主线程和后台线程都能安全使用同一个 NetworkManager 实例,无需手动加锁。

值类型走另一条路:structenum 默认满足 Sendable,跨任务传递时复制语义自动避免共享(16:56)。先用值类型,搞不定共享可变状态再上 actor,是 Apple 推荐的优先级。


核心启发

  • 做什么:在新建 iOS 19 项目时确认开启 Main Actor 默认模式

    • 为什么值得做:Xcode 26 新模板默认开启,编译器自动给 UI 模块的代码加 @MainActor,省掉一堆显式标注,访问 UI 单例和全局状态不会再被报”data race”。
    • 怎么开始:在 Build Settings 里搜 “Default Actor Isolation”,确认是 MainActor。老项目可以从 UI 相关的 module 开始切换,业务逻辑 module 暂时保持原状。
  • 做什么:把 URLSession.shared.data(...) 同步调用全部替换成 async/await

    • 为什么值得做:同步网络 IO 在主线程几乎肯定造成 UI 卡顿。async 版本不需要你引入并发,只要 await 一下就把卡顿点变成挂起点。
    • 怎么开始:搜代码里所有 Data(contentsOf:) 和同步的 URLSession 用法,把调用方改成 async 函数,用 Task { } 在按钮回调里启动。
  • 做什么:用 Instruments 找出主线程上真正耗时的计算,再决定加 @concurrent

    • 为什么值得做:演讲明确说”很多 app 不需要并发”。盲目把所有耗时操作搬到后台只会让代码变复杂、引入新的数据竞争问题,应该先用 profiler 定位瓶颈。
    • 怎么开始:在 Instruments 里跑 Time Profiler,找到主线程占用最高的函数。如果确实是 CPU 密集(比如图像解码、JSON 解析大数据),给那个函数加 @concurrent,让编译器告诉你它访问了哪些隔离状态。
  • 做什么:把跨线程共享的可变引用类型重构为 actor

    • 为什么值得做:单例式的 class 一旦被多线程访问就需要手动加锁,容易写错。actor 把同步原语内置在语言层,外部调用必须 await,编译器帮你拦住所有数据竞争。
    • 怎么开始:列出项目里被多个线程访问的 manager / cache 类,逐个改成 actor。注意外部调用全部要 await,可能需要把调用链上的函数都改成 async
  • 做什么:用值类型 + Sendable 处理跨任务数据传递

    • 为什么值得做:struct 默认满足 Sendable,复制语义天然避免共享,比 actor 简单得多。Apple 在演讲里把”值类型优先”列为第一原则。
    • 怎么开始:检查代码里跨任务传递的数据,能用 struct 表达就别用 class。引用类型如果一定要传,确认它是不可变的并标 Sendable,否则改成值类型快照。

关联 Session

评论

GitHub Issues · utterances