概述
在 WWDC 24 中苹果推出了全新的 Observation 框架,借助于它我们可以更加细粒度的监听可观察(@Observable)对象 。同时,SwiftUI 自身也与时偕行开始全面支持 @Observable 对象的“嵌入”。
然而在这里,我们却另辟蹊径来介绍 @Observable 对象另外一些“鲜为人知”的故事。
在本篇博文中,您将学到如下内容:
- 适配 Swift 6 所经历的“荆棘载途”
- 鞭辟入里:问题的根源及解决
相信学完本课后,小伙伴们一定会对“自立自强”的可观察对象的使用更加游刃有余、运用自如。
那还等什么呢?Let’s go!!!;)
4. 适配 Swift 6 所经历的“荆棘载途”
虽然上述实现在 Swift 5 的编译器中运行得相当不错,但如果我们尝试在 Swift 6 中编译它,实际上则会遇到一些尴尬的局面。
一旦我们选择 Swift 6 语言版本,立即会让编译器“怨声载道”:
func observe() {
withObservationTracking { [weak self] in
guard let self else { return }
// Capture of'self' with non-sendable type 'CounterObserver?' in a `@Sendable` closure
print("counter.count: \(counter.count)")
}
}
这里的错误信息表明:withObservationTracking 方法要求我们传递一个 @Sendable 类型的闭包,这意味着我们不能捕获非 Sendable 属性。
然而,我们不能直接将闭包改为非 Sendable 类型,因为我们是在系统框架中 withObservationTracking 方法的 onChange 闭包中使用它的,而且正如大家可能猜到的那样,onChange 要求我们的闭包必须是 Sendable 类型的。
在大多数情况下,我们可以通过使用 @MainActor 修饰符来使得 self 成为 Sendable 对象。这样的话,对象就总是在主线程上进行其属性访问和方法调用了。
有时候这并不是一个坏主意,但当我们尝试在示例中应用它时,将会收到如下错误:
@MainActor
class CounterObserver {
let counter: Counter
init(counter: Counter) {
self.counter = counter
}
func observe() {
withObservationTracking { [weak self] in
guard let self else { return }
// Main actor-isolated property 'counter' can not be referenced from a Sendable closure
print("counter.count: \(counter.count)")
}
}
}
此时,我们可以继续通过将属性访问操作包装(Wrap)在一个同样在主线程上运行的 Task 中来使代码编译通过,但这样做的结果是我们将以异步方式访问 counter,并且会丢失一些传入的事件。
那么,除了使用 @unchecked Sendable 来迫使 CounterObserver 成为 Sendable 类型以外:
class CounterObserver: @unchecked Sendable {...}
我们还有没有其它解决办法呢?
5. 鞭辟入里:问题的根源及解决
为了得出 Swift 6 中这个编译问题的根本原因,我们需要回过头来看看 withObservationTracking 方法的实际签名:
@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
public func withObservationTracking<T>(_ apply: () -> T, onChange: @autoclosure () -> @Sendable () -> Void) -> T
从上面的方法签名可以看到:实际上 withObservationTracking 只有第二个闭包才有 Sendable 限定,而第一个完全不用遵守 Sendable 约定。
回忆一下我们重构后的辅助方法,@Sendable 闭包 execute 被 withObservationTracking 方法中的两个闭包先后调用了,这就是问题的根本之所在!
所以,解决此问题的一种方法是将调用拆开,完全跳过重构后的包装方法:
@MainActor
class CounterObserver {
let counter: Counter
init(counter: Counter) {
self.counter = counter
}
func observe() {
withObservationTracking {[weak self] in
guard let self else { return }
print("counter.count: \(counter.count)")
} onChange: {
DispatchQueue.main.async {
self.observe()
}
}
}
}
当然,在 onChange 闭包中我们也可以用 Task 来同样达到异步执行的目的:
func observe() {
withObservationTracking {[weak self] in
guard let self else { return }
print("counter.count: \(counter.count)")
} onChange: {
Task {
try? await Task.sleep(for: .seconds(0.01))
await self.observe()
}
}
}
如果可以舍弃取消操作,我们甚至可以干脆用 actor 来实现整个 CounterObserver 类:
actor CounterObserver {
let counter: Counter
init(counter: Counter) {
self.counter = counter
}
func observe() {
withObservationTracking {
print("counter.count: \(counter.count)")
} onChange: {
Task {
try? await Task.sleep(for: .seconds(0.01))
await self.observe()
}
}
}
}
let counter = Counter()
let monitor = CounterObserver(counter: counter)
Task {
await monitor.observe()
}
counter.count += 1
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
counter.count += 1
}
不过,这时必须 await monitor.observe() 方法的调用,所以自然我们会丢失一个 counter.count 属性初始化的监听事件了:
看到这里,小伙伴们是否对 Observation 框架以及 @Observable 可观察对象的监听有了更深刻的领悟呢?那还不赶紧给自己一个大大的赞,么么哒!
总结
在本篇博文中,我们讨论了之前 withObservationTracking 包装方法为何会在 Swift 6 的严格并发模式中被编译器“人怨神怒”,并最终给出解决方案。
感谢观赏,再会啦!8-)