欢迎回来,这一节开始,我们先编写一个新的测试用例,验证 MergeInput 合并订阅多个 Publishers 的能力。在 CustomCombineTests.swift 里,添加下面的代码:

func testMergeInput() {
  let subject1 = PassthroughSubject<Int, Never>()
  let subject2 = PassthroughSubject<Int, Never>()
  let input = MergeInput<Int>()

  subject1.merge(into: input)
  subject2.merge(into: input)

  var received = [Subscribers.Event<Int, Never>]()

  let sink = input.sink(receiveCompletion: {
    received.append(.complete($0))
  }, receiveValue: {
    received.append(.value($0))
  })

  subject1.send(sequence: 1...2, completion: .finished)
  subject2.send(sequence: 3...4, completion: .finished)

  XCTAssertEqual(received, [1, 2, 3, 4].asEvents(completion: nil))

  sink.cancel()
}

大体上,它和 testMultipleSubjectSubscribe 是一样的,只不过我们把订阅上游 Publisher 的 PassthroughSubject 换成了 MergeInput。这次,我们就无需再手工管理用于订阅的 Cancellable 对象了,也不用再刻意构建 subject1 和 subject2 的事件内容了。当 subject1 和 subject2 结束之后,received 收录的就是我们期望的 [1, 2, 3, 4] 事件。

无法“续命”的订阅

了解了同时订阅多个 Publisher 的行为之后,我们再来探讨另外一种情况:当一个订阅者订阅的 Publisher 结束之后,还可以用它继续订阅其它 Publisher 么?为了验证这个过程,我们先来实现这个测试用例的前半部分:

func testSinkReactivation() {
  var received = [Subscribers.Event<Int, Never>]()
  let sink = Subscribers.Sink<Int, Never>(receiveCompletion: {
    received.append(.complete($0))
  }, receiveValue: {
    received.append(.value($0))
  })

  weak var weakSubject: PassthroughSubject<Int, Never>?

  do {
    let subject = PassthroughSubject<Int, Never>()
    weakSubject = subject
    subject.subscribe(sink)

    subject.send(1)
  }

  XCTAssertNotNil(weakSubject)
}

其中,sink 是我们用于测试的订阅者,它订阅了 do 作用域中的 subject。按照之前说过的内容,离开作用域之后,subject 并不会被释放,订阅也依然有效,此时的 weakSubject 并不是 nil。接下来,通过 weakSubject,我们让事件序列结束:

func testSinkReactivation() {
  /// The same as before...

  weakSubject?.send(completion: .finished)
  XCTAssertNil(weakSubject)
  XCTAssertEqual(received, [1].asEvents(completion: .finished))
}

这次,weakSubject 就是 nil 了,并且 received 中应该可以收录到订阅的事件 1 以及 .finished。

接下来,按照常理,后面就没有 subject 和 sink 什么事儿了。但这时,如果我们用 sink 重新订阅一个新的 Publisher 会如何呢?继续在 testSinkReactivation 里添加下面的代码试一下:

func testSinkReactivation() {
  /// The same as before...

  let subject2 = PassthroughSubject<Int, Never>()
  subject2.subscribe(sink)
  subject2.send(2)

  XCTAssertEqual(received, [1].asEvents(completion: .finished))
}

就像测试中判断依据所展示的,sink 已经无法再订阅到 subject2 中的事件。也就是说,一旦订阅结束了,就是结束了。不过要说明一下的是,这个行为仅在 mac OS 10.15 beta6 及以后的版本才生效。在 beta6 之前,sink6 是可以订阅到 subject2 的事件的。

What's next?

以上,就是所有关于 Combine 中有关重新订阅这个行为的研究。在众多发现里,最重要的一个,就是订阅代码有可能会把 Publisher 和 Subscriber 保持在内存里,进而造成意料之外的内存泄漏。使用 sink 方法订阅相对安全,因为它返回的 AnyCancellable 对象会自动取消订阅。而如果要使用自定义的 Subscribers.Sink 对象,就要格外小心,手工处理好 cancel 的调用。或者,确保事件序列的正常结束。

说到这,我们对 Combine 中的各种边缘情况,整理的就差不多了。接下来,就是最接近在泊学 App 开发中遇到的一个问题了。你认为一旦 Subscriber 订阅了 Publisher 之后,就可以收到它的所有事件了么?或者说,一旦 Subscriber 取消了订阅,它就再也不会订阅到到事件了么?

让人意外的是,在 Combine 里,这两个问题的答案都是 No。在接下来的内容里,我们就来研究下这些场景,导致问题发生的原因,以及避免的方法。

所有订阅均支持 12 期免息分期

¥ 59

按月订阅

一个月,观看并下载所有视频内容。初来泊学,这可能是个最好的开始。

开始订阅

¥ 512

按年订阅

一年的时间,让我们一起疯狂地狩猎知识吧。比按月订阅优惠 28%。

开始订阅

¥ 1280

泊学终身会员

永久观看和下载所有泊学网站视频,并赠送 100 元商店优惠券。

我要加入
如需帮助,欢迎通过以下方式联系我们