【问题标题】:iOS Swift Combine: cancel a Set<AnyCancellable>iOS Swift 组合:取消 Set<AnyCancellable>
【发布时间】:2020-03-19 00:19:28
【问题描述】:

如果我将可取消集存储到 ViewController 中:

private var bag = Set<AnyCancellable>()

其中包含多个订阅。

1 - 我应该在 deinit 中取消订阅吗?还是它自动完成这项工作?

2 - 如果是这样,我如何取消所有存储的订阅?

bag.removeAll() is enough?

或者我应该遍历集合并一一取消所有订阅?

for sub in bag {
   sub.cancel()
}

Apple 表示订阅是有效的,直到存储的 AnyCancellable 在内存中。所以我想用bag.removeAll() 释放cancellables 就足够了,不是吗?

【问题讨论】:

    标签: ios swift swiftui ios13 combine


    【解决方案1】:

    deinit 上,您的 ViewController 将从内存中删除。它的所有实例变量都将被释放。

    Combine &gt; Publisher &gt; assign(to:on:) 的文档说:

    一个 AnyCancellable 实例。当你没有时在这个实例上调用 cancel() 不再希望发布者自动分配属性。 取消初始化此实例也会取消自动分配。

    1 - 我应该在 deinit 中取消订阅吗?还是它会自动完成这项工作?

    您不需要,它会自动完成这项工作。当您的 ViewController 被释放时,实例变量 bag 也将被释放。由于没有更多引用您的AnyCancellable,因此作业将结束。

    2 - 如果是这样,我如何取消所有已存储的订阅?

    并非如此。但通常您可能有一些订阅要开始和停止,例如,viewWillAppear/viewDidDissapear。在这种情况下,您的 ViewController 仍在内存中。

    因此,在viewDidDissappear 中,您可以按照您的猜测执行bag.removeAll()。这将删除引用并停止分配。

    您可以运行以下代码来查看 .removeAll() 的实际效果:

    var bag = Set<AnyCancellable>()
    
    func testRemoveAll() {
      Timer.publish(every: 1, on: .main, in: .common).autoconnect()
        .sink { print("===== timer: \($0)") }
        .store(in: &bag)
    
      Timer.publish(every: 10, on: .main, in: .common).autoconnect()
        .sink { _ in self.bag.removeAll() }
        .store(in: &bag)
    }
    

    第一个计时器将每隔一秒触发一次并打印出一行。第二个计时器将在 10 秒后触发,然后调用 bag.removeAll()。然后两个计时器发布者都将停止。

    https://developer.apple.com/documentation/combine/publisher/3235801-assign

    【讨论】:

      【解决方案2】:

      如果您碰巧从您的视图控制器订阅了发布者,您可能会在sink 中捕获self,这将引用它,并且如果订阅者稍后不会让 ARC 删除您的视图控制器没有完成,所以建议弱捕捉自我


      所以而不是:

         ["title"]
            .publisher
            .sink { (publishedValue) in
              self.title.text = publishedValue
          }
      
      .store(in: &cancellable)
      

      你应该使用[weak self]:

         ["title"]
            .publisher
            .sink { [weak self] (publishedValue) in
              self?.title.text = publishedValue
          }
      
      .store(in: &cancellable)
      

      因此,当视图控制器被移除时,您不会有任何保留周期或内存泄漏。

      【讨论】:

      • 这如何回答他的问题?
      • @giorashc 用户询问他是否应该手动取消订阅或“它会自动完成这项工作”,我的回答显示了如果没有对 self 的任何强烈引用,您如何避免考虑订阅。通过这种方式,订阅将被自动删除。
      【解决方案3】:

      尝试创建一个管道并将可取消项存储在某个状态变量中。你会发现管道一遇到异步操作就停止了。那是因为 ARC 清理了 Cancellable,因此它被自动取消了。因此,如果您释放对管道的所有引用,则无需在管道上调用取消。

      来自documentation

      AnyCancellable 实例在取消初始化时会自动调用 cancel()。

      【讨论】:

      • 它似乎不像文档所说的那样工作。我测试了 syncRequest().sink().store(in: &disposables) 并在 viewmodel 上定义它并将 deinit { } 添加到视图模型。 deinit 每次切换屏幕时都会打印,但订阅 receiveCancel 不会被调用,并且 receiveValue 会被多次调用
      • @MichałZiobro 听起来像是 stackoverflow 的一个好问题:D
      • subscriptions.removeAll() 在 Swift 5.4 中运行良好
      【解决方案4】:

      我测试了这段代码

      let cancellable = Set<AnyCancellable>()
      
      Timer.publish(every: 1, on: .main, in: .common).autoconnect()
        .sink { print("===== timer: \($0)") }
        .store(in: &cancellable)
        
      cancellable.removeAll() // just remove from Set. not cancellable.cancel()
      

      所以我使用了这个扩展。

      import Combine
      
      typealias CancelBag = Set<AnyCancellable>
      
      extension CancelBag {
        mutating func cancelAll() {
          forEach { $0.cancel() }
          removeAll()
        }
      }
      

      【讨论】:

      • 我认为你的意思是“从集合中删除”与“数组”
      【解决方案5】:

      创建一个Cancelable+Extensions.swift

      import Combine
      
      typealias DisposeBag = Set<AnyCancellable>
      
      extension DisposeBag {
          mutating func dispose() {
              forEach { $0.cancel() }
              removeAll()
          }
      }
      

      在你的实现类中,在我的例子中 CurrentWeatherViewModel.swift 只需添加 disposables.dispose() 以删除 SetAnyCancellable

      import Combine
      import Foundation
      
      final class CurrentWeatherViewModel: ObservableObject {
          @Published private(set) var dataSource: CurrentWeatherDTO?
      
          let city: String
          private let weatherFetcher: WeatherFetchable
          private var disposables = Set<AnyCancellable>()
      
          init(city: String, weatherFetcher: WeatherFetchable = WeatherNetworking()) {
              self.weatherFetcher = weatherFetcher
              self.city = city
          }
      
          func refresh() {
              disposables.dispose()
      
              weatherFetcher
                  .currentWeatherForecast(forCity: city)
                  .map(CurrentWeatherDTO.init)
                  .receive(on: DispatchQueue.main)
                  .sink(receiveCompletion: { [weak self] value in
                      guard let self = self else { return }
                      switch value {
                      case .failure:
                          self.dataSource = nil
                      case .finished:
                          break
                      }
                      }, receiveValue: { [weak self] weather in
                          guard let self = self else { return }
                          self.dataSource = weather
                  })
                  .store(in: &disposables)
          }
      }
      

      【讨论】:

      • 为什么要为集合中的每个AnyCancelable 显式调用cancel?只需调用 removeAll() 就足以将它们设置为 nil 并取消正在进行的订阅者任务。
      猜你喜欢
      • 1970-01-01
      • 2020-09-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-07-23
      • 2018-05-25
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多