欢迎回来,这一节开始,我们先编写一个新的测试用例,验证 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。在接下来的内容里,我们就来研究下这些场景,导致问题发生的原因,以及避免的方法。