自从 Swift 并发模型首次引入 async 和 await 关键字以来,我就迫不及待的开始使用它们来进行异步代码的开发了。随着时间的推移,Swift 并发模型变得越来越强大,它通过让 Swift 编译器识别潜在问题,提供了可靠的数据竞争安全保障。
然而,在切换到 Swift 6 版本后,大家面对代码中自动生成的所有警告和错误可能会显得束手无策。这里,我们将分享一些在代码库中适配 Swift 6 严格并发模式(Strict Concurrency)的小技巧,以充分利用代码安全性所带来的优势。
我们的代码常需要纳新吐故,而本篇的谆谆善诱必将使小伙伴们受益匪浅、满载而归!那还等什么呢?让我们马上开始 Adopting Swift 6 之旅吧!
Let‘s go!!!
在 Swift 6 的适配之路上,编译器会频繁抱怨有关可发送性(sendability) 这一问题,因为这是数据竞争的主要原因。
数据竞争发生在一段代码写入内存,而同时另一段代码读取同一块内存的引用时。在这种情况下,我们“可耐”的 App 可能会因为奇怪的 EXC_BAD_ACCESS
错误而榱栋崩折。
通常,数据竞争发生在多个线程共享一个类的实例对象时,并且其中至少有一个线程对其进行了写入操作。
为了避免这种情况,我们在代码库中应该尽量减少类的使用。下面就让我们来看看具体如何躬行实践吧。
如果某个对象是不可修改的,那就不可能发生数据竞争。 这一原则鼓励我们尽可能多地使用结构体。
例如,我们可以将所有数据类型定义为可发送(Sendable)且不可变(Immutable)的结构体:
public struct Statistics: Sendable, Hashable {
public let value: Double
public let interval: DateInterval
public init(value: Double, interval: DateInterval) {
self.value = value
self.interval = interval
}
}
在上面的示例代码中,我们将 Statistics 定义为 Sendable 结构体。在大多数情况下,结构体可以隐式推断其自身的可发送性,但当我们将其定义为 public 时,就需要显式地声明。
通过遵循 Sendable 协议,我们可以让编译器懂得:某个类型的实例可以安全地在线程之间共享。
不仅数据类型可以是结构体,我们也能将无状态(Stateless)的服务类型定义为结构体:
public struct HealthService: Sendable {
private let store: HKHealthStore
public init(store: HKHealthStore) {
self.store = store
}
public var age: Int {
guard
let dateOfBirthComponents = try? store.dateOfBirthComponents(),
let dateOfBirth = Calendar.current.date(from: dateOfBirthComponents)
else {
return 0
}
let age = Calendar.current.dateComponents([.year], from: dateOfBirth, to: .now)
return age.year ?? 0
}
}
结构体不仅“本领高强”,而且创建成本也比类更低。在 Swift 6 中,小伙伴们可以怡然自得地在线程之间传递 Sendable 结构体,而不会遇到任何警告或数据竞争错误。因此,我们强烈建议尽可能优先将结构体整合到自己的代码库中去。
虽说结构的优点众多,但整个应用的开发无法完全脱离引用类型。类(Class)非常适用于一个特定情形:它允许我们共享状态,而无需复制它。
在 App 中,一组视图可能依赖于单个共享状态,并需要在这些视图之间保持同步。不幸的是,结构体此时无法满足需求,因为每个视图都会获得结构体实例的一个副本,而这通常绝不是我们想要的结果。
我们在应用中应该尽量将类的使用限制在与视图相关( view-related)的场景中,例如:视图模型(View Model)或代理(Delegate)类型。由于它们与视图相关,因此我们需要使用 @MainActor 对它们进行修饰。
关于全局 Actor 的特性及其好处,小伙伴们可以参考我的 Swift 结构化并发之全局 Actor 趣谈 一文。
全局 Actor 是使类型隐式 Sendable 的另一种方式。当某个类型被隔离到一个全局 Actor 时,它存储的数据将不会被同时读写,这多亏了 Actor 的强大威力:
@MainActor @Observable final class Store<State, Action> {
private(set) var state: State
private let reduce: (State, Action) -> State
init(
initialState state: State,
reduce: @escaping (State, Action) -> State
) {
self.state = state
self.reduce = reduce
}
func send(_ action: Action) {
state = reduce(state, action)
}
}
除了数据类型、无状态服务和视图相关的内容以外,我们的应用里还有一类对象属于有状态服务类型的范畴。比如:一个缓存搜索结果的搜索服务,它可能会被多个线程同时使用,并且需要维护缓存的并发访问状态。
actor SearchService {
private var history = Tree<Food>()
func search() async throws -> [Food] {
// read history
// make network request
// mutate history
}
}
这种类型是应用中潜在的数据竞争来源,但值得庆幸的是,通过使用单个 actor 关键字可以轻松化解它们于无形。正如大家已经知道的那样:actor 同样可以保护其状态,并允许互斥访问,从而消除数据竞争。
如果小伙伴们想了解更多关于 Swift 中 Actor 的知识,可以移步如下链接观赏精彩的内容:
Swift 是一门优秀且性感的语言,它能够让我们撸出简洁且安全的代码。Swift 同样提供了一套全面的工具集,来帮助大家轻松实现自己内心深处的“小目标”,棒棒哒!
想要进一步系统地学习 Swift 开发的小伙伴们,可以来我的《Swift 语言开发精讲》专栏逛一逛哦:
在本篇博文中,我们讨论了如何让自己项目中的旧代码全面适配 Swift 6 的基本原则和一些小技巧,相信大家定能手到擒来。
感谢观赏,再会啦!