Kotlin withTimeout协程取消

Kotlin withTimeout coroutine cancellation

我第一次尝试协程函数withTimeout。我正在尝试从 Android 中的设备 GPS 获取当前位置,并在没有可用位置的情况下添加超时。我无法控制获取位置的过程,因此无法轻松取消它。

更新:我最终得到了自定义超时逻辑,因为 android 本机 api 不可取消:

suspend fun LocationManager.listenLocationUpdate(): Location? = 
   withTimeoutOrNull(TIMEOUT) {
       locationManager.listenLocationUpdate("gps")
   }

  private suspend fun LocationManager.listenLocationUpdate(provider: String) =
        suspendCoroutine<Location?> { continuation ->
            requestLocationUpdates(provider, 1000, 0f, object: TimeoutLocationListener{
                override fun onLocationChanged(location: Location?) {
                    continuation.resume(location)
                    this@listenLocationUpdate.removeUpdates(this)
                }
            })
        }

所以请求位置的过程属于sdk,我不能轻易取消它。有什么建议吗?

要使 withTimeout[OrNull] 正常工作,您需要一个协作式可取消协程。如果您调用的函数是阻塞的,它将不会按预期工作。调用协程根本不会恢复,更不用说停止阻塞方法的处理了。您可以检查 this playground code 来确认这一点。

如果要构建可取消的基于协程的 API,首先必须有一个可取消的 API。但是,如果不知道您正在调用的确切函数,就很难回答您的问题。

使用Android的LocationManager,你可以将getCurrentLocation包装成一个可取消的挂起函数(此函数仅在API 30+级可用) :

@RequiresApi(Build.VERSION_CODES.R)
@RequiresPermission(anyOf = [permission.ACCESS_COARSE_LOCATION, permission.ACCESS_FINE_LOCATION])
suspend fun LocationManager.getCurrentLocation(provider: String, executor: Executor): Location? = suspendCancellableCoroutine { cont ->
    val signal = CancellationSignal()

    getCurrentLocation(provider, signal, executor) { location: Location? ->
        cont.resume(location)
    }

    cont.invokeOnCancellation {
        signal.cancel()
    }
}

否则,您也可以使用 callbackFlow 将基于侦听器的 API 转换为可取消的基于流的 API,取消订阅(通过删除侦听器):

@OptIn(ExperimentalCoroutinesApi::class)
@RequiresPermission(anyOf = [permission.ACCESS_COARSE_LOCATION, permission.ACCESS_FINE_LOCATION])
fun LocationManager.locationUpdates(provider: String, minTimeMs: Long, minDistance: Float = 0f): Flow<Location> =
    callbackFlow {
        val listener = LocationListener { location -> sendBlocking(location) }
        requestLocationUpdates(provider, minTimeMs, minDistance, listener)

        awaitClose {
            removeUpdates(listener)
        }
    }

如果您只想更新一次,可以在返回的流程上使用 first(),这将自动支持取消:

suspend fun LocationManager.listenLocationUpdate(): Location? = 
   withTimeoutOrNull(TIMEOUT) {
       locationManager.locationUpdates("gps", 1000).first()
   }

如果您在位置请求中使用 numUpdates = 1,您也应该能够将基于侦听器的 API 包装到单次暂停功能中。此处的取消​​可以通过删除侦听器来完成。