kotlin-协程与网络请求结果回调浅析

协程与网络请求的结合已经不是新鲜事物,那网络请求的结果是如何在协程中回调的呢?

本文简单探讨使用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,继续追踪CancellableContinuationinvokeOnCancellation方法,源码如下:

/**
 * ....略...
 * 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也能及时的回调协程取消的操作,协程取消后,网络请求也取消了。

四、总结

文章是借用网络请求来理解协程的回调,以及取消协程应该注意的问题,可以举一反三以点带面来思考其他场景协程的使用。 协程想要学精还是挺难的,需要一点点积累,一点点总结。如果有发现错误或者不足欢迎指出。

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
在这里插入图片描述
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

全套视频资料:

一、面试合集

在这里插入图片描述
二、源码解析合集
在这里插入图片描述

三、开源框架合集
在这里插入图片描述
欢迎大家一键三连支持,若需要文中资料,直接扫描文末CSDN官方认证微信卡片免费领取↓↓↓

PS:群里还设有ChatGPT机器人,可以解答大家在工作上或者是技术上的问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值