由@Observable类管理HKLiveWorkoutBuilder的并发问题排查
问题场景
我正在开发HealthController类,用于封装HealthKit的辅助方法,后续将支持心率、卡路里等指标追踪。当前基于Swift 6完整并发模式实现,类标记为@MainActor @Observable以适配SwiftUI视图调用,但在调用builder?.beginCollection(at:)和builder?.endCollection(at:)时出现以下警告:
Sending main actor-isolated value of type 'HKLiveWorkoutBuilder' with later accesses to nonisolated context risks causing data races
尝试过将builder标记为nonisolated(与@Observation特性冲突)、用MainActor.run包裹调用,均未解决问题。
原始代码
import HealthKit import SwiftUI @MainActor @Observable final class HealthController { // State let store = HKHealthStore() var session: HKWorkoutSession? var builder: HKLiveWorkoutBuilder? // Other code for authorization, configuration defaults etc... func startWatchWorkout(_ name: String, _ startDate: Date) async { do { let configuration = HKWorkoutConfiguration() configuration.activityType = .traditionalStrengthTraining session = try HKWorkoutSession(healthStore: store, configuration: configuration) builder = session?.associatedWorkoutBuilder() builder?.dataSource = HKLiveWorkoutDataSource(healthStore: store, workoutConfiguration: configuration) session?.startActivity(with: startDate) try await builder?.beginCollection(at: startDate) } catch { // ... } } func endWatchWorkout(_ endDate: Date) async { do { session?.end() try await builder?.endCollection(at: endDate) session = nil builder = nil } catch { // ... } } }
修复方案
方案1:使用局部变量捕获builder引用
核心思路是将builder赋值给MainActor上下文中的局部变量,避免编译器担心属性被跨线程访问。修改startWatchWorkout和endWatchWorkout方法:
func startWatchWorkout(_ name: String, _ startDate: Date) async { do { let configuration = HKWorkoutConfiguration() configuration.activityType = .traditionalStrengthTraining session = try HKWorkoutSession(healthStore: store, configuration: configuration) builder = session?.associatedWorkoutBuilder() builder?.dataSource = HKLiveWorkoutDataSource(healthStore: store, workoutConfiguration: configuration) session?.startActivity(with: startDate) // 捕获局部变量,消除逃逸风险 let localBuilder = builder try await localBuilder?.beginCollection(at: startDate) } catch { // ... } } func endWatchWorkout(_ endDate: Date) async { do { session?.end() // 捕获局部变量,消除逃逸风险 let localBuilder = builder try await localBuilder?.endCollection(at: endDate) session = nil builder = nil } catch { // ... } }
方案2:标记session和builder为非隔离并忽略观察
如果session和builder不需要被SwiftUI视图观察,可以用@ObservationIgnored和nonisolated修饰,彻底隔离并发上下文:
@MainActor @Observable final class HealthController { let store = HKHealthStore() // 标记为非隔离并忽略视图观察 @ObservationIgnored nonisolated var session: HKWorkoutSession? @ObservationIgnored nonisolated var builder: HKLiveWorkoutBuilder? // 其余代码不变... }
这种方法更彻底,因为HKWorkoutSession和HKLiveWorkoutBuilder本身不是MainActor绑定的类型,将它们设为非隔离更符合HealthKit的设计,同时@ObservationIgnored解决了与@Observable的冲突。
内容的提问来源于stack exchange,提问作者Ilja

