第10章:调试¶
了解异步代码中的事件流一直是一个挑战。在Combine
中尤其如此,因为发布者中的操作符链可能不会立即发出事件。例如,像throttle(for:scheduler:latest:)
这样的运算符不会发送他们收到的所有事件,因此您需要了解发生了什么。Combine
提供了一些运算符来帮助调试您的被动流。了解它们将帮助您对令人困惑的情况进行故障排除。
打印活动¶
当您不确定是否有任何东西通过您的发布者时,print(_:to:)
运算符是您应该使用的第一个运算符。这是一个直通式发布者,可以打印大量关于正在发生的事情的信息。
即使有像这样的简单情况:
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
对象。您可以使用它来重定向字符串以打印到记录器。您还可以向日志添加信息,例如当前日期和时间等。可能性是无穷无尽的!
例如,您可以创建一个简单的记录器,显示每个字符串之间的时间间隔,以便了解发布者发送值的速度:
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 }
结果显示每行打印之间的时间:
+0.00111s: publisher: receive subscription: (1...3)
+0.03485s: publisher: request unlimited
+0.00035s: publisher: receive value: (1)
+0.00025s: publisher: receive value: (2)
+0.00027s: publisher: receive value: (3)
+0.00024s: publisher: receive finished
如上所述,这里的可能性是无穷无尽的。
Note
根据计算机和您运行此代码的Xcode
版本,上面打印的间隔可能会略有不同。
对事件采取行动-执行副作用¶
除了打印信息外,对特定事件执行操作通常也很有用。我们称之为执行副作用,因为您“侧面”采取的行动不会直接影响流中的更多发布者,但可能会产生修改外部变量等效果。
handleEvents(receiveSubscription:receiveOutput:receiveCompletion:receiveCancel:receiveRequest:)
(哇,多么好的签名!)让您可以拦截发布者生命周期中的所有事件,然后在每个步骤中采取行动。
想象一下,您正在跟踪发布者必须执行网络请求,然后发送一些数据的问题。当您运行它时,它永远不会收到任何数据。发生什么事了?这个请求真的有效吗?你甚至看到有什么回来了吗?
考虑以下代码:
let request = URLSession.shared
.dataTaskPublisher(for: URL(string: "https://www.raywenderlich.com/")!)
request
.sink(receiveCompletion: { completion in
print("Sink received completion: \(completion)")
}) { (data, _) in
print("Sink received data: \(data)")
}
你运行它,永远不会看到任何打印。你能通过查看代码来看出问题吗?
如果没有,请使用 handleEvents
来跟踪正在发生的事情。 你可以在发布者和sink
之间插入这个操作符:
.handleEvents(receiveSubscription: { _ in
print("Network request will start")
}, receiveOutput: { _ in
print("Network request data received")
}, receiveCancel: {
print("Network request cancelled")
})
然后,再次运行代码。这次您看到一些调试输出:
Network request will start
Network request cancelled
那里!你找到了:你忘了把Cancellable
放在身边。因此,订阅开始,但会立即取消。现在,您可以通过保留Cancellable
来修复代码:
let subscription = request
.handleEvents...
然后,再次运行代码,您现在会看到它运行正常:
Network request will start
Network request data received
Sink received data: 303094 bytes
Sink received completion: finished
使用调试器作为最后手段¶
最后的操作符是您在调试器中的某些时候真正需要反省的情况,因为没有其他东西能帮助您弄清楚出了什么问题。
第一个简单的运算符是breakpointOnError()
。顾名思义,当您使用此运算符时,如果任何上游发布者发出错误,Xcode
将中断调试器,让您查看堆栈,并希望找到发布者出错的原因和位置。
一个更完整的变体是breakpoint(receiveSubscription:receiveOutput:receiveCompletion:)
它允许您拦截所有事件,并根据具体情况决定是否要暂停调试器。
例如,只有当某些值通过发布者时,您才能中断:
.breakpoint(receiveOutput: { value in
return value > 10 && value < 15
})
假设上游发布者发出整数值,但值11
到14
永远不会发生,您可以将breakpoint
配置为仅在这种情况中断裂,并让您进行调查!您还可以有条件地中断订阅和完成时间,但不能像handleEvents
操作符那样拦截取消。
Note
没有一个断点发布者会在Xcode playground
中工作。您将看到一个错误,表明执行已中断,但它不会落入调试器,因为Playground
通常不支持断点调试。
关键点¶
print
操作符一起跟踪发布者的生命周期,- 创建您自己的
TextOutputStream
来自定义输出字符串, - 使用
handleEvents
操作符拦截生命周期事件并执行操作, - 使用
breakpointOnError
和breakpoint
运算符在特定事件上中断。
接下来去哪?¶
你发现了如何跟踪你的发布者在做什么,现在是时候......计时器了!转到下一章,了解如何使用Combine
定期触发事件。