了解异步代码中的事情流,对于初学者来说一直是一个挑战。在Combine的上下文中特别如此,由于事情流中的操作符链或许不会当即宣布事情。
例如,throttle(for:scheduler:latest:)
操作符就不会宣布接收到的任何事情,要了解这个进程中产生了什么,就需要借助Combine提供的一些操作符来进行调试,以协助咱们解决遇到的困难。
当你不确定流中产生的事情时,首先能够考虑运用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
操作符阻拦生命周期事情,并履行操作 - 运用
breakpointOnError
和breakpoint
操作符来中止特定事情
参考
Combine: Asynchronous Programming with Swift