SwiftUI 多线程

NotificationCenter

NotificationCenter 是 iOS macOS 中最经典的 发布订阅 通信机制:

一个对象发送通知(Post),任意多个对象接收通知(Observer)。

SwiftUI 虽然有 @State@Observable、Environment 等现代数据流方式,但 NotificationCenter 仍然非常有用

NotificationCenter.default.publisher() 返回创建的 NotificationCenter.Publisher。

NotificationCenter 基本结构

        post()

      NotificationCenter

      ┌──────┴──────┐
      │             │
 Observer A     Observer B
      │             │
收到通知        收到通知

NotificationCenter 使用样例

import SwiftUI

// MARK: - 通知名称
extension Notification.Name {
    static let userDidLogin = Notification.Name("userDidLogin")
}

// MARK: - 数据模型
struct User {
    let name: String
}

struct ContentView: View {

    @State private var status = "未登录"
    @State private var username = "-"
    @State private var age = 0

    var body: some View {

        VStack(spacing: 20) {

            Text("状态:\(status)")
                .font(.title2)

            Text("用户名:\(username)")

            Text("年龄:\(age)")

            Divider()

            Button("发送登录通知(object + userInfo)") {

                let user = User(name: "Tom")

                NotificationCenter.default.post(
                    name: .userDidLogin,
                    object: user,
                    userInfo: [
                        "username": "Tom",
                        "age": 18
                    ]
                )
            }

        }
        .padding()

        // 接收通知
        .onReceive(
            NotificationCenter.default.publisher(
                for: .userDidLogin
            )
        ) { notification in

            print("收到通知")

            // 读取 object
            if let user = notification.object as? User {
                print("object -> \(user.name)")
                status = "\(user.name) 登录成功"
            }

            // 读取 userInfo
            if let info = notification.userInfo {

                if let name = info["username"] as? String {
                    username = name
                    print("username -> \(name)")
                }

                if let age = info["age"] as? Int {
                    self.age = age
                    print("age -> \(age)")
                }
            }
        }
    }
}

#Preview {
    ContentView()
}

整个流程图

                    Button


NotificationCenter.default.post(...)

          ┌────────────┴────────────┐
          │                         │
      object                   userInfo
   User(name:"Tom")     ["username":"Tom",
                             "age":18]
          │                         │
          └────────────┬────────────┘

NotificationCenter.default.publisher(...)

                 .onReceive()

      ┌────────────────┴────────────────┐
      │                                 │
notification.object              notification.userInfo
      │                                 │
 User(name:"Tom")               username、age
      │                                 │
      └────────────────┬────────────────┘

                更新 @State


              SwiftUI 自动刷新界面

NotificationCenter 监听系统通知

例如:

import SwiftUI

struct ContentView: View {

    @Environment(\.scenePhase)
    private var scenePhase

    @State private var logs: [String] = []

    var body: some View {

        NavigationStack {

            List(logs.reversed(), id: \.self) { log in
                Text(log)
            }
            .navigationTitle("App 生命周期 Demo")
        }

        // MARK: NotificationCenter

        // App 已进入后台
        .onReceive(
            NotificationCenter.default.publisher(
                for: UIApplication.didEnterBackgroundNotification
            )
        ) { _ in

            addLog("Notification:进入后台")
        }

        // App 即将进入前台
        .onReceive(
            NotificationCenter.default.publisher(
                for: UIApplication.willEnterForegroundNotification
            )
        ) { _ in

            addLog("Notification:即将进入前台")
        }

        // App 已经激活
        .onReceive(
            NotificationCenter.default.publisher(
                for: UIApplication.didBecomeActiveNotification
            )
        ) { _ in

            addLog("Notification:App Active")
        }

        // MARK: scenePhase

        .onChange(of: scenePhase) { _, newValue in

            switch newValue {

            case .active:
                addLog("scenePhase:active")

            case .inactive:
                addLog("scenePhase:inactive")

            case .background:
                addLog("scenePhase:background")

            @unknown default:
                addLog("scenePhase:unknown")
            }
        }
    }

    private func addLog(_ text: String) {

        let formatter = DateFormatter()
        formatter.dateFormat = "HH:mm:ss"

        let time = formatter.string(from: Date())

        logs.append("[\(time)] \(text)")

        print("[\(time)] \(text)")
    }
}

#Preview {
    ContentView()
}
NotificationCenter.default.publisher()



NotificationCenter.Publisher


        ├──────────────► .onReceive

        ├──────────────► .map(...)

        ├──────────────► .compactMap(...)

        ├──────────────► .filter(...)

        ├──────────────► .dropFirst(...)

        ├──────────────► .debounce(...)

        └──────────────► .throttle(...)

不要阻塞UI

移动应用程序的UI对用户无响应是绝不可接受的,但有时你需要执行一些可能需要花费超过几毫秒的操作。 最显着的是……访问网络。 另一个例子……进行一些非常消耗CPU的分析,例如机器学习。当这些长期运行的任务在进行时,我们如何保持UI的响应能力?

我们在一个与UI正在运行的线程不同的“执行线程”上执行它们。

Threads 线程

大多数现代操作系统(包括 iOS)都允许你指定要使用的“执行线程”。这些线程看起来似乎都在同时执行它们的代码。

去追踪什么代码在什么时候、什么地方运行,可能会非常棘手且艰难。在 Swift 中,系统在幕后为我们管理着这些执行线程!

这使得作为 Swift 程序员的我们,工作变得远没有那么棘手和艰难。但我们必须给系统提供一些关于它应该如何做到这一点的提示。

Swift 并发基础

我们了解了使用线程来实现并发的好处,但其潜在的陷阱是什么?

数据竞争/数据竞态。在多个线程上同时进行读写的一块数据可能会遭到损坏。Swift 是如何帮助你确保这种情况绝不发生的?

  1. actor 模型与隔离
  2. “等待”与“挂起”
  3. 同步与异步执行环境的区别

actors

Swift 通过又一种新的数据类型来同步对数据的访问:actor。

actor 是一种引用类型(就像类 class 一样),但它不支持继承。因此,除了它具备引用类型的语义之外,它看起来非常像一个结构体 struct。但是 actor 最主要的“核心本领”在于,它能够同步对其内部所有变量(vars)和函数(funcs)的访问。如果你不进行多线程编程,你永远不需要创建自己的 actor。如果你的多线程操作需要与 UI 协同工作,你可以通过 MainActor 来同步你的代码。

那么,一个 actor 是如何跨多个执行线程同步所有数据访问的呢?

首先,在任何给定的时间,actor 内部仅能有一个函数(或变量访问)处于运行状态。其次,所有函数都会运行至完毕,除非它们被挂起(以便稍后重新恢复执行)。由于绝不会有两个函数同时运行,因此 actor 的数据不会发生任何冲突。并且由于它们总是运行至完毕,或者运行到有明确标记的挂起点,因此它是可预测的。这就是同步!

我们称运行在某个 actor 上的函数或变量,对该 actor 是隔离的(isolated)。很多代码都是非隔离的(nonisolated),即不绑定到任何 actor。例如,你的数据模型(Model)在大部分时间里可能都是非隔离运行的。但 UI 中的代码总是隔离在一个被称为 MainActor 的 actor 中。

挂起

为了确保可预测性,你必须让 Swift 知道什么时候可能会发生挂起。因此,你必须做的一件事就是,使用关键字 async 来标记任何可以被挂起的函数。

因此,可以被挂起的函数被称为“异步函数(async functions)”。执行诸如网络访问等操作的函数,几乎总是异步(async)的。

因为它们在等待网络数据返回的过程中是可以被挂起的。async 是一种声明,表示该函数内部“可能”会发生挂起,而不是说一定会发生。

一个非常耗时的函数可能会将自己标记为 async,即使它并不会真正发生挂起。将一个函数标记为 async,会强制调用者去注意这件事

async 和 await

当你调用一个 async(异步)函数时,你必须用关键字 await 来标记该调用。当你调用一个与你处于不同 actor 中的函数时,你同样需要使用 await。

因为当你调用时,该 actor 可能正在另一个线程上执行它的其他代码。这样做是合理的,因为你将会等待(await)该异步(async)操作的完成。

关键问题在于:当一个函数执行到带有 await 的代码行时,它可能会被挂起!事实上,一个 actor 中的函数会被挂起的唯一情况,就是它在 await 另一个调用。 否则,该函数会一直运行至完毕。

当一个 actor 内部的函数被挂起时,该 actor 中的其他代码是被允许运行的。你只能在处于异步执行上下文的代码中使用 await。这意味着你要么处于一个自身被标记为 async 的函数中,要么你处于一个 Task 闭包中。

await 存在的全部原因,就是为了提醒你自己:你可能会在这里被挂起。你无法预知自己在那个 await 处会被挂起多长时间。 它可能是一段非常长的时间(或者也可能是零延迟)。并且当你恢复执行时,你所在的 actor 中的其他函数在此期间可能已经遭到调用了。

确保你考虑到这种可能性,是属于你自己的职责。

Sendable 可传递协议

如果一个 actor 将一个参数传递给另一个 actor,并且它们都对该参数进行修改会怎么样?

那绝对会引发并发问题!

因此,如果你想跨越 actor 的隔离边界来传递参数,那么这些函数参数需要是 Sendable 的(即遵循了 Sendable 协议)。

Task

大多数代码都是同步运行的(即其自身不涉及多线程问题)。在那种同步上下文中,你是不被允许使用 await 的。

那么,如果你需要调用一个 async(异步)函数该怎么办?你可以通过使用一个 Task 来实现这一点。

let task = Task(priority: TaskPriority? = nil) {
    // 执行一些需要你使用 await 或者可能需要耗费很长时间的操作
}

上面这行代码会立即返回。它会在“稍后的某个时间”执行闭包体(快则立即可执行,但绝不会永远不执行)。

返回的 task 实例允许你对该任务进行取消、让步、休眠或等待其返回结果值。

但通常情况下,我们只是让它直接在后台运行。闭包中的代码在运行时,将具有与声明 let task = 处相同的 actor 隔离属性。

async closures 异步闭包

如果你将一个闭包作为参数传递,而该闭包可能需要使用 await,会发生什么?在这种情况下,你所传递闭包的那个目标函数,必须将其对应的参数标记为 async。

有一些视图修饰符(View modifiers)会接受异步闭包作为参数……

Main Actor

我们最希望保护其免受多线程干扰的重要“数据结构”就是UI(用户界面)。因此,存在一个专门为 UI 服务的 actor。

它被称为“主 actor(main actor)”。所有的 UI 活动都必须在主 actor 上进行。你所有的视图(View)函数和计算属性(computed vars)都会自动在主 actor 上运行。

然而,你的数据模型(Model)则不会。

@Observable 机制会在 MainActor(主 Actor) 上执行更新。

因此,只有当你的 Model 自己需要进行多线程工作时,你才真正需要使用 actor 隔离(actor isolation)。

在这种情况下,你通常可以直接把 MainActor 当作同步(Synchronization)的 actor。

你可以通过给函数、变量,或者整个 @Observable 类型添加 @MainActor 来做到这一点。

给函数、变量或类标记 @MainActor,意味着它们会被同步到 UI 所在的 actor(MainActor) 上执行。

只要你的代码不是执行耗时操作(time-consuming),这是一个开销很小、实现简单的同步方式。

但如果你正在进行重量级(heavy-duty)的多线程工作,那么你可能应该创建自己的 actor。

@Observable 本身不能是一个 actor,但你可以创建一个专门负责异步工作的 actor。

然后使用 Task 将工作派发到这个专用 actor 中执行,并等待(await)它完成。

这也是 SwiftUI 官方目前推荐的架构:

切换到 MainActor 的方式

方式一:Task + @MainActor

Task { @MainActor in
    model.count += 1
}

等价于:创建一个新的异步任务,并保证这个任务在 MainActor 上运行。例如

Button("更新") {
    Task { @MainActor in
        count += 1
    }
}

方式二:MainActor.run

await MainActor.run {
    count += 1
}

它不会创建新的 Task,它只是:把当前任务切换到 MainActor,执行这一小段代码,然后继续执行。

func load() async {
    let data = await fetchData()

    await MainActor.run {
        self.items = data
    }

    print("继续后台工作")
}
后台线程

    │ fetchData()

拿到数据

    │ await MainActor.run

MainActor
更新 UI


返回后台
继续执行
Task { @MainActor in } await MainActor.run
创建新的异步任务 不创建新任务
不需要 await 必须 await
不等待里面执行完成 会等待执行完成再继续
更适合”启动一个任务” 更适合”切换一下线程执行一小段代码”

XCode 设置 Strict Concurrency Checking

Swift Compiler 支持 并发静态检查,需要项目设置中找到

Swift Compiler - Concurrency 打开 Strict Concurrency Checking

有 Minial、Targeted、Complete 三种。

综合 Demo

代码

import SwiftUI
import Observation

//======================================================
// MARK: - Sendable Model
//======================================================

struct User: Identifiable, Sendable {
    let id = UUID()
    let name: String
}

//======================================================
// MARK: - Heavy Worker Actor
//
// actor 保证内部数据不会发生 Data Race
// 所有耗时工作放这里
//======================================================

actor Worker {

    private var requestCount = 0

    /// 模拟网络请求
    func fetchUsers() async -> [User] {

        requestCount += 1

        print("开始网络请求 \(requestCount)")

        // 模拟等待网络
        try? await Task.sleep(for: .seconds(2))

        return [
            User(name: "Tom"),
            User(name: "Lucy"),
            User(name: "Jack"),
            User(name: "Rose"),
            User(name: "Mike")
        ]
    }

    /// 模拟 CPU 密集型工作
    func heavyCalculation() async -> Int {

        print("开始CPU计算")

        var sum = 0

        for i in 0..<20_000_000 {
            sum += i % 10
        }

        print("CPU计算完成")

        return sum
    }
}

//======================================================
// MARK: - Observable Model
//
// 官方推荐:Observable 放 MainActor
//======================================================

@MainActor
@Observable
class AppModel {

    var users: [User] = []

    var loading = false

    var calculationResult = 0

    var status = "Idle"

    private let worker = Worker()

    //==================================================
    // async 函数
    //==================================================

    func loadUsers() async {

        loading = true
        status = "Loading..."

        // await 调用 actor
        let result = await worker.fetchUsers()

        users = result

        loading = false

        status = "Network Finished"
    }

    //==================================================
    // 演示 MainActor.run
    //==================================================

    func calculate() {

        Task {

            status = "Calculating..."

            let result = await worker.heavyCalculation()

            // 当前 Task 可能已经不在 MainActor
            // 切回来更新 UI
            await MainActor.run {

                calculationResult = result

                status = "Calculation Finished"
            }

            print("继续后台工作...")
        }
    }

    //==================================================
    // 演示 Task { @MainActor in }
    //==================================================

    func updateStatusLater() {

        Task {

            try? await Task.sleep(for: .seconds(1))

            Task { @MainActor in

                status = "Updated by Task @MainActor"

            }

        }
    }
}

//======================================================
// MARK: - View
//
// View 永远运行 MainActor
//======================================================

struct ContentView: View {

    @State
    private var model = AppModel()

    @State
    private var task: Task<Void, Never>?

    var body: some View {

        NavigationStack {

            List {

                Section("状态") {

                    Text(model.status)

                    if model.loading {
                        ProgressView()
                    }

                    Text("CPU结果")

                    Text("\(model.calculationResult)")
                }

                Section("用户") {

                    ForEach(model.users) { user in

                        Text(user.name)

                    }

                }

                Section("Task") {

                    Button("开始网络请求") {

                        // 在同步环境中调用 async
                        task = Task {

                            await model.loadUsers()

                        }

                    }

                    Button("CPU计算(不卡UI)") {

                        model.calculate()

                    }

                    Button("Task + @MainActor") {

                        model.updateStatusLater()

                    }

                    Button("取消当前Task") {

                        task?.cancel()

                        model.status = "Cancelled"

                    }

                }

            }
            .navigationTitle("Swift Concurrency Demo")

            //--------------------------------------------------
            // View 生命周期 Task
            //--------------------------------------------------

            .task {

                print(".task 自动执行")

                await model.loadUsers()

            }

            //--------------------------------------------------
            // 下拉刷新
            //--------------------------------------------------

            .refreshable {

                await model.loadUsers()

            }

        }

    }

}

//======================================================

#Preview {

    ContentView()

}

运行时流程

程序启动:

ContentView


.task


model.loadUsers()


await Worker.fetchUsers()

      │ suspend

等待2秒


返回 User[]


MainActor 更新UI

整个过程中:

UI

│ 可以滚动
│ 可以点击按钮
│ 可以动画

└─────────────── 没有被阻塞

因为等待网络期间发生的是 await,而不是 sleep() ,await 会挂起 Suspend 当前任务,不会阻塞线程,更不会卡住 MainActor。

CPU 计算流程

点击:CPU 计算(不卡UI),流程

Button


Task


Worker Actor


heavyCalculation()

(2000万次循环)



返回 Int


await MainActor.run


更新UI

因为 Worker 是一个独立的 actor,耗时不会占用 MainActor,不会阻塞UI。