// 添加恢复协程执行的监听器
lastLocation.addOnSuccessListener { location ->
// 恢复协程并返回位置
continuation.resume(location)
}.addOnFailureListener { e ->
// 通过抛出异常来恢复协程
continuation.resumeWithException(e
)
}
// suspendCancellableCoroutine 块的结尾。这里会挂起协程
//直到某个回调调用了 continuation 参数
}
注意: 尽管协程库中同样包含了不可取消版本的协程构建器 (即 suspendCoroutine
),但最好始终选择使用 suspendCancellableCoroutine
处理协程作用域的取消及从底层 API 传播取消事件。
suspendCancellableCoroutine 原理
在内部,suspendCancellableCoroutine 使用 suspendCoroutineUninterceptedOrReturn 在挂起函数的协程中获得 Continuation。这一 Continuation
对象会被一个 CancellableContinuation 对象拦截,后者会从此时开始控制协程的生命周期 (其 实现 具有 Job 的功能,但是有一些限制)。
接下来,传递给 suspendCancellableCoroutine
的 lambda 表达式会被执行。如果该 lambda 返回了结果,则协程将立即恢复;否则协程将会在 CancellableContinuation 被 lambda 手动恢复前保持挂起状态。
您可以通过我在下面代码片段 (原版实现) 中的注释来了解发生了什么:
public suspend inline fun suspendCancellableCoroutine(
crossinline block: (CancellableContinuation) -> Unit
): T =
// 获取运行此挂起函数的协程的 Continuation 对象
suspendCoroutineUninterceptedOrReturn { uCont ->
// 接管协程。Continuation 已经被拦截,
// 接下来将会遵循 CancellableContinuationImpl 的生命周期
val cancellable = CancellableContinuationImpl(uCont.intercepted(), …)
/* … */
// 使用可取消 Continuation 调用代码块
block(cancellable)
// 挂起协程并且等待 Continuation 在 “block” 中被恢复,或者在 “block” 结束执行时返回结果
cancellable.getResult()
}
想了解更多有关挂起函数的工作原理,请参阅这篇: Kotlin Vocabulary | 揭秘协程中的 suspend 修饰符。
流数据
如果我们转而希望用户的设备在真实的环境中移动时,周期性地接收位置更新 (使用 requestLocationUpdates) 函数),我们就需要使用 Flow 来创建数据流。理想的 API 看起来应该像下面这样:
fun FusedLocationProviderClient.locationFlow(): Flow
为了将基于回调的 API 转换为 Flow,可以使用 callbackFlow 流构建器来创建新的 flow。callbackFlow
的 lambda 表达式的内部处于一个协程的上下文中,这意味着它可以调用挂起函数。不同于 flow 流构建器,channelFlow 可以在不同的 CoroutineContext 或协程之外使用 offer 方法发送数据。
通常情况下,使用 callbackFlow 构建流适配器遵循以下三个步骤:
- 创建使用 offer 向 flow 添加元素的回调;
- 注册回调;
- 等待消费者取消协程,并注销回调。
将上述步骤应用于当前用例,我们得到以下实现:
// 发送位置更新给消费者
fun FusedLocationProviderClient.locationFlow() = callbackFlow {
// 创建了新的 Flow。这段代码会在协程中执行。
// 1. 创建回调并向 flow 中添加元素
val callback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult?) {
result ?: return // 忽略为空的结果
for (location in result.locations) {
try {
offer(location) // 将位置发送到 flow
} catch (t: Throwable) {
// 位置无法发送到 flow
}
}
}
}
// 2. 注册回调并通过调用 requestLocationUpdates 获取位置更新。
requestLocationUpdates(
createLocationRequest(),
callback,
Looper.getMainLooper()
).addOnFailureListener { e ->
close(e) // 在出错时关闭 flow
}
// 3. 等待消费者取消协程并注销回调。这一过程会挂起协程,直到 Flow 被关闭。
awaitClose {
// 在这里清理代码
removeLocationUpdates(callback)
}
}
callbackFlow 内部原理
在内部,callbackFlow 使用了一个 channel。channel 在概念上很接近阻塞 队列) —— 它在配置时需要指定容量 (capacity): 即可以缓冲的元素个数。在 callbackFlow 中创建的 channel 默认容量是 64 个元素。如果将新元素添加到已满的 channel,由于 offer 不会将元素添加到 channel 中,并且会立即返回 false,所以 send 会暂停生产者,直到频道 channel 中有新元素的可用空间为止。
awaitClose 内部原理
有趣的是,awaitClose
内部使用的是 suspendCancellableCoroutine
。您可以通过我在以下代码片段中的注释 (查看 原始实现) 一窥究竟:
public suspend fun ProducerScope<*>.awaitClose(block: () -> Unit = {}) {
…
try {
// 使用可取消 continuation 挂起协程
suspendCancellableCoroutine { cont ->
// 仅在 Flow 或 Channel 关闭时成功恢复协程,否则保持挂起
invokeOnClose { cont.resume(Unit) }
}
} finally {
// 总是会执行调用者的清理代码
block()
}
}
复用 Flow
除非额外使用中间操作符 (如: conflate
),否则 Flow 是冷且惰性的。这意味着每次调用 flow 的终端操作符时,都会执行构建块。对于我们的用例来说,由于添加一个新的位置监听器开销很小,所以这一特性不会有什么大问题。然而对于另外的一些实现可就不一定了。
您可以使用 shareIn
中间操作符在多个收集器间复用同一个 flow,并使冷流成为热流。
val FusedLocationProviderClient.locationFlow() = callbackFlow {
…
}.shareIn(
// 让 flow 跟随 applicationScope
使冷流成为热流。
val FusedLocationProviderClient.locationFlow() = callbackFlow {
…
}.shareIn(
// 让 flow 跟随 applicationScope