了解异步代码中的事情流,对于初学者来说一直是一个挑战。在Combine的上下文中特别如此,由于事情流中的操作符链或许不会当即宣布事情。

例如,throttle(for:scheduler:latest:) 操作符就不会宣布接收到的任何事情,要了解这个进程中产生了什么,就需要借助Combine提供的一些操作符来进行调试,以协助咱们解决遇到的困难。

Print

当你不确定流中产生的事情时,首先能够考虑运用print(_:to:)操作符来进行打印操作,它是一个passthrough publisher,能够打印很多事情流信息,协助咱们了解事情传输进程中产生了什么,比方用它来了解事情流的生命周期。

let subscription = (1 ... 3).publisher
    .print("publisher")
    .sink { _ in }

控制台会输出流中产生的事情:

publisher: receive subscription: (1...3)
publisher: request unlimited
publisher: receive value: (1)
publisher: receive value: (2)
publisher: receive value: (3)
publisher: receive finished

print(_:to:)还承受一个 TextOutputStream 目标,咱们能够运用它来重定向字符串并打印出来。通过自定义的TextOutputStream,在日志中增加信息,例如当前日期和时刻等。

比方:

class TimeLogger: TextOutputStream {
    private var previous = Date()
    private let formatter = NumberFormatter()
    init() {
        formatter.maximumFractionDigits = 5
        formatter.minimumFractionDigits = 5
    }
    func write(_ string: String) {
        let trimmed = string.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmed.isEmpty else {
            return
        }
        let now = Date()
        print("+\(formatter.string(for: now.timeIntervalSince(previous))!)s: \(string)")
        previous = now
    }
}

运用:

let subscription = (1 ... 3).publisher
    .print("publisher", to: TimeLogger())
    .sink { _ in }

handleEvents

除了打印事情信息之外,运用handleEvents(receiveSubscription:receiveOutput:receiveCompletion:receiveCancel:receiveRequest:)对特定事情进行操作也很有用,它不会直接影响下游其他publisher,但会产生类似于修正外部变量的效果。所以能够称其为“履行副作用”。

handleEvents能够让你阻拦一个publisher生命周期内的任何事情,并且能够在每一步对它们进行操作。

幻想一下,你正在盯梢的publisher必须履行网络恳求,然后宣布一些数据。但当你运转它时,却怎么也收不到数据,比方下面的代码便是如此,

let request = URLSession.shared.dataTaskPublisher(for: URL(string: "https://kodeco.com/")!)
request.sink(receiveCompletion: { completion in
    print("Sink received completion: \(completion)")
}) { data, _ in
    print("Sink received data: \(data)")
}

运转之后,控制台没有任何输出。你是否能发现这段代码存在的问题呢?

如果问题你没找到,那么就能够运用 handleEvents来盯梢并查找问题所在。

request.handleEvents(receiveSubscription: { _ in
    print("恳求开端了")
}, receiveOutput: { _ in
    print("恳求到数据了")
}, receiveCancel: {
    print("恳求撤销了")
}).sink(receiveCompletion: { completion in
    print("Sink received completion: \(completion)")
}, receiveValue: { data, _ in
    print("Sink received data: \(data)")
})

再次履行,能够看到打印:

恳求开端了
恳求撤销了

由于Subscriber 回来的是一个AnyCancellable 目标,如果不持有这个目标,那么它会立刻被撤销(开释),这儿的问题便是没有持有 Cancellable 目标,导致publisher被提早开释了, 修正代码:

let subscription = request.handleEvents(receiveSubscription: { _ in
    print("恳求开端了")
}, receiveOutput: { _ in
    print("恳求到数据了")
}, receiveCancel: {
    print("恳求撤销了")
}).sink(receiveCompletion: { completion in
    print("Sink received completion: \(completion)")
}, receiveValue: { data, _ in
    print("Sink received data: \(data)")
})

再次运转,打印如下:

恳求开端了
恳求到数据了
Sink received data: 266785 bytes
Sink received completion: finished

终极大招

当你竭尽浑身解数也无法找到问题所在时,“万不得已”操作符是协助你解决问题的终极方案。

简略的“万不得已”操作符: breakpointOnError() 。 望文生义,运用此运算符时,如果任何上游publisher宣布过错,Xcode 将中止调试器,让你从仓库中找出publisher出错的原因和方位。

完好的变体是 breakpoint(receiveSubscription:receiveOutput:receiveCompletion:) 。 它允许你阻拦一切事情并根据具体情况决议是否要暂停调试器。比方,只有当某些值通过publisher时才能够中止:

.breakpoint(receiveOutput: { value in
    value > 10 && value < 15
})

假定上游publisher宣布整数值,但值 11 到 14 永久不会产生,就能够将断点装备为仅在这种情况下中止,以进行检查。 你还能够有条件地中止订阅和完成时刻,但不能像 handleEvents 运算符那样阻拦撤销。

总结

以上咱们现已介绍了几种Combine的调试办法,下面做一个简略的总结:

  • 运用print操作符盯梢publisher的生命周期
  • 创建自定义的TextOutputStream来输出需要的调试信息
  • 运用handleEvents操作符阻拦生命周期事情,并履行操作
  • 运用breakpointOnErrorbreakpoint操作符来中止特定事情

参考

Combine: Asynchronous Programming with Swift