协程与网络请求的结合已经不是新鲜事物,那网络请求的结果是如何在协程中回调的呢?
本文简单探讨使用suspendCoroutine,suspendCancellableCoroutine,CompletableDeferred在请求中的回调,如有不足欢迎评论纠正或补充。
一、suspendCoroutine
suspendCoroutine可以暴露协程的回调Continuation,这样我们就可以通过这个回调设置网络请求的返回,先看下面的代码,是一个简单的okhttp请求:
class MainOneActivity : AppCompatActivity() {
private var startTime: Long = 0
private var endTime: Long = 0
private val mainViewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//打印协程名称
System.setProperty("kotlinx.coroutines.debug", "on")
//感谢wanandroid api
val url = "https://www.wanandroid.com//hotkey/json"
//简单的okhttp网络请求
val client = OkHttpClient()
val request = Request.Builder().url(url).build()
val call = client.newCall(request)
//记录开始时间
startTime = System.currentTimeMillis()
//开启协程发起网络请求
mainViewModel.viewModelScope.launch {
val deprecated = async(Dispatchers.IO) {
//协程中网络数据的回调
callbackData(call)
}
//异常的回调
deprecated.invokeOnCompletion {
endTime = System.currentTimeMillis()
//打印异常信息
log("invokeOnCompletion: ${endTime - startTime}ms error: $it")
}
//获取到数据的回调
deprecated.await().let {
endTime = System.currentTimeMillis()
//打印正常信息
log("await: ${endTime - startTime}ms data: $it")
}
}
}
/**
* 协程中网络数据的回调
*/
private suspend fun callbackData(call: Call): String =
suspendCoroutine { continuation ->
//okhttp网络请求
call.enqueue(object : okhttp3.Callback {
override fun onFailure(call: Call, e: IOException) {
//回调异常信息
continuation.resumeWithException(e)
}
override fun onResponse(call: Call, response: Response) {
val data = response.body?.string() ?: ""
//回调正常信息
continuation.resume(data)
//打印网络请求的结果
log("onResponse: $data")
}
})
}
/**
* 打印日志
*/
fun log(msg: String) {
Log.d("LOG_PRINT",
"""
-
内容:$msg,
线程:${Thread.currentThread().name}
""".trimIndent())
}
}
整个代码比较简单,就是通过协程发起网络请求,然后回调网络请求的结果,其中需要注意的点是通过ViewModel开启的协程作用域(主线程),然后通过async开启子线程的协程作用域,等待子线程的协程作用域挂起恢复后,结果会回调到主线程。具体注释已经很清晰,看一下日志输出的结果:
LOG_PRINT: -
内容:invokeOnCompletion: 342ms error: null, //没有异常信息
线程:DefaultDispatcher-worker-1 @coroutine#2
LOG_PRINT: -
内容:onResponse: {"data":[{"id":6,"link":""....略...}, //网络请求成功
线程:OkHttp https://www.wanandroid.com/...
LOG_PRINT: -
内容:await: 346ms data:{"data":[{"id":6,"link":""....略...}, //主线程回调网络请求的结果
线程:main @coroutine#1
成功的实现了整个网络请求的回调过程,接下来测试一下取消网络请求,代码如下:
//开启协程发起网络请求
mainViewModel.viewModelScope.launch {
val deprecated = async(Dispatchers.IO) {
//协程中网络数据的回调
callbackData(call)
}
delay(100) //<-------------------------------变化在这里
deprecated.cancel() //<-------------------------------变化在这里
//异常的回调
deprecated.invokeOnCompletion {
if(deprecated.isCancelled){ //<-------------------------------变化在这里
//协程被取消的时候取消网络请求
call.cancel() //<-------------------------------变化在这里
}
endTime = System.currentTimeMillis()
log("invokeOnCompletion: ${endTime - startTime}ms error: $it")
}
//获取到数据的回调
deprecated.await().let {
endTime = System.currentTimeMillis()
log("await: ${endTime - startTime}ms data: $it")
}
}
日志输出:
LOG_PRINT: -
内容:invokeOnCompletion: 598ms error: kotlinx.coroutines.JobCancellationException: DeferredCoroutine was cancelled; job="coroutine#2":DeferredCoroutine{Cancelled}@26b4072,
线程:DefaultDispatcher-worker-1 @coroutine#2
LOG_PRINT: -
内容:onResponse: data:{"data":[{"id":6,"link":""....略...}, //网络请求仍然成功了,且耗时没有减少
线程:OkHttp https://www.wanandroid.com/...
一般情况下都是在invokeOnCompletion中监听协程的取消回调,所以把网络请求的取消写在了回调里面,但是发现网络请求仍然执行了,整个耗时并没有减少,所以在平时项目中应该谨慎使用。相对于suspendCoroutine,其实有一个可取消的回调函数可以用,那就是suspendCancellableCoroutine。
二、suspendCancellableCoroutine
这里直接看使用suspendCancellableCoroutine取消网络请求的代码,其他基本雷同:
class MainTwoActivity : AppCompatActivity() {
private var startTime: Long = 0
private var endTime: Long = 0
private val mainViewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//打印协程名称
System.setProperty("kotlinx.coroutines.debug", "on")
//感谢wanandroid api
val url = "https://www.wanandroid.com/article/top/json"
val client = OkHttpClient()
val request = Request.Builder().url(url).build()
val call = client.newCall(request)
//记录开始时间
startTime = System.currentTimeMillis()
//开启协程发起网络请求
mainViewModel.viewModelScope.launch {
val deprecated = async {
callbackWithCancelData(call)
}
//延迟100毫秒后取消网络请求
delay(100)
deprecated.cancel() <---------------------------取消协程
//异常的回调
deprecated.invokeOnCompletion {
endTime = System.currentTimeMillis()
log("invokeOnCompletion: ${endTime - startTime}ms error: $it ")
}
//获取到数据的回调
deprecated.await().let {
endTime = System.currentTimeMillis()
log("await: ${endTime - startTime}ms data: $it ")
}
}
}
/**
* 协程中网络数据的回调
*/
private suspend fun callbackWithCancelData(call: Call): String =
suspendCancellableCoroutine { continuation -> <--------------------改为suspendCancellableCoroutine
call.enqueue(object : okhttp3.Callback {
override fun onFailure(call: Call, e: IOException) {
continuation.resumeWithException(e)
}
override fun onResponse(call: Call, response: Response) {
val data = response.body?.string() ?: ""
continuation.resume(data)
log("onResponse: $data")
}
})
//取消协程回调(suspendCoroutine 没有这个api)
continuation.invokeOnCancellation { <---------------------------取消协程的回调
//取消网络请求
call.cancel()
}
}
/**
* 打印日志
*/
fun log(msg: String) {
Log.d("LOG_PRINT",
"""
-
内容:$msg,
线程:${Thread.currentThread().name}
""".trimIndent())
}
}
输出日志:
LOG_PRINT: -
内容:invokeOnCompletion: 112ms error: kotlinx.coroutines.JobCancellationException: DeferredCoroutine was cancelled; job="coroutine#2":DeferredCoroutine{Cancelled}@f0d4e55 ,
线程:main @coroutine#1
可以看到网络请求被取消了,且耗时只有112毫秒,所以suspendCancellableCoroutine更适合我们在协程取消时需要同步取消其他任务的需求。为什么suspendCoroutine没有invokeOnCancellation这个api,而suspendCancellableCoroutine有呢? 对比源码看一下:
public suspend inline fun <T> suspendCoroutine(crossinline block: (Continuation<T>) -> Unit): T {
contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
return suspendCoroutineUninterceptedOrReturn { c: Continuation<T> ->
val safe = SafeContinuation(c.intercepted())
block(safe)
safe.getOrThrow()
}
}
public suspend inline fun <T> suspendCancellableCoroutine(
crossinline block: (CancellableContinuation<T>) -> Unit
): T =
suspendCoroutineUninterceptedOrReturn { uCont ->
val cancellable = CancellableContinuationImpl(uCont.intercepted(), resumeMode = MODE_CANCELLABLE)
cancellable.initCancellability()
block(cancellable)
cancellable.getResult()
}
原来suspendCoroutine的协程回调对象是Continuation, 而suspendCancellableCoroutine的协程回调对象是CancellableContinuation,继续追踪CancellableContinuation的invokeOnCancellation方法,源码如下:
/**
* ....略...
* the handler will be invoked as soon as this
* continuation is cancelled.
* ....略...
*/
public fun invokeOnCancellation(handler: CompletionHandler)
可以看到api的介绍中,如果协程被取消会尽快的回调这个函数,于是我们就可以在这个api中做协程取消的同步动作了。
接下来看一下CompletableDeferred。
三、CompletableDeferred
直接看使用CompletableDeferred取消网络请求的代码,如下:
class MainThreeActivity : AppCompatActivity() {
private var startTime: Long = 0
private var endTime: Long = 0
private val mainViewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//打印协程名称
System.setProperty("kotlinx.coroutines.debug", "on")
//感谢wanandroid api
val url = "https://www.wanandroid.com/article/top/json"
val client = OkHttpClient()
val request = Request.Builder().url(url).build()
val call = client.newCall(request)
//记录开始时间
startTime = System.currentTimeMillis()
//开启协程发起网络请求
mainViewModel.viewModelScope.launch {
//协程中网络数据的回调
val deprecated = callbackWithDeferredCancelData(call) <-----------网络请求
//延迟100毫秒后取消协程
delay(100)
deprecated.cancel() <-----------取消协程
//异常的回调
deprecated.invokeOnCompletion {
//取消协程
if (deprecated.isCancelled) { <-----------监听取消协程
//取消网络请求
call.cancel() <-----------取消网络请求
}
endTime = System.currentTimeMillis()
log("invokeOnCompletion: ${endTime - startTime}ms error:$it")
}
//获取到数据的回调
deprecated.await().let {
endTime = System.currentTimeMillis()
log("await: ${endTime - startTime}ms data: $it ")
}
}
}
/**
* 协程中网络数据的回调(不需要是一个挂起函数)
*/
private fun callbackWithDeferredCancelData(call: Call): CompletableDeferred<String> {
return CompletableDeferred<String>().also { deferred -> <-----------使用CompletableDeferred
call.enqueue(object : okhttp3.Callback {
override fun onFailure(call: Call, e: IOException) {
deferred.completeExceptionally(e) <-----------回调数据
}
override fun onResponse(call: Call, response: Response) {
val data = response.body?.string() ?: ""
if (response.isSuccessful) {
deferred.complete(data) <-----------回调数据
} else {
deferred.completeExceptionally(Exception()) <-----------回调数据
}
log("onResponse: $data ")
}
})
}
}
/**
* 打印日志
*/
fun log(msg: String) {
Log.d(
"LOG_PRINT",
"""
-
内容:$msg,
线程:${Thread.currentThread().name}
""".trimIndent()
)
}
}
日志输出:
LOG_PRINT: -
内容:invokeOnCompletion: 110ms error:kotlinx.coroutines.JobCancellationException: Job was cancelled; job=CompletableDeferredImpl{Cancelled}@f0d4e55,
线程:main @coroutine#1
可以看到CompletableDeferred也能及时的回调协程取消的操作,协程取消后,网络请求也取消了。
四、总结
文章是借用网络请求来理解协程的回调,以及取消协程应该注意的问题,可以举一反三以点带面来思考其他场景协程的使用。 协程想要学精还是挺难的,需要一点点积累,一点点总结。如果有发现错误或者不足欢迎指出。
调用await的时候要包一层,如果正常执行的结果是异常,会回调到await,如下:
runCatching {
deferred.await()
}.onFailure { throwable ->
onError("$throwable")
}.onSuccess { result ->
onFinish(result)
}