Combine框架@Published属性潜在竞态问题及最佳方案咨询
@Published属性更新的竞态问题及解决最佳实践
问题根源:跨线程内存可见性竞态
你遇到的确实是竞态条件导致的异常。原因在于:
fetchAllNotifications里的网络请求在异步Task的后台队列执行,赋值notifications = ...也发生在后台线程。- @Published的机制是赋值完成后立即向订阅者发送新值,且你通过
receive(on: DispatchQueue.main)把回调切换到了主队列。但此时主队列线程的内存缓存可能还没同步到viewModel.notifications的最新值——也就是说,sink回调拿到的notifs是@Published确保的最新数据,但直接访问viewModel.notifications时,主队列还没读取到后台线程更新后的属性值,就出现了两者count不一致的情况。 - 你的表格视图大概率直接以
viewModel.notifications作为数据源,此时调用reloadData()会读取到旧的空数组,导致表格显示为空。
最佳实践
方案1:确保ViewModel属性的赋值在主队列执行
把网络请求的结果切换到主队列后再赋值,让属性更新和UI回调处于同一个队列,彻底消除竞态:
func fetchAllNotifications() { Task { do { let fetchedNotifications = try await NotificationsService.shared.getAllNotifications() // 切换到主Actor上下文赋值,符合Swift Concurrency规范 await MainActor.run { self.notifications = fetchedNotifications } } catch { printError(error) } } }
方案2:直接使用sink回调的参数作为UI数据源
在ViewController中维护本地的数据源副本,完全依赖@Published发送的新值更新UI,不再直接访问ViewModel的属性:
class NotificationsViewController: UIViewController { private let viewModel = NotificationsViewModel() private var currentNotifications = [NotificationData]() // 本地数据源 private var cancellables = Set<AnyCancellable>() // ... 其他UI配置代码 func bindToViewModel() { viewModel.fetchAllNotifications() viewModel.$notifications .receive(on: DispatchQueue.main) .sink { [weak self] newNotifications in self?.currentNotifications = newNotifications self?.tableView.reloadData() } .store(in: &cancellables) } // 表格数据源方法使用本地副本 func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return currentNotifications.count } }
这种方式完全遵循Combine的设计意图:订阅者只通过回调接收最新状态,不依赖对发布者原始属性的直接访问,从根源上避免了内存可见性问题。
不推荐的方案
你提到的didSet手动通知或延迟调用reloadData()属于临时 workaround,没有解决本质的竞态问题,反而增加了代码复杂度,不建议使用。
内容的提问来源于stack exchange,提问作者Shuaiqing Luo
相关产品推荐
相关产品推荐

