使用kotlin 封装SDK将回调改写为协程以便调用

377 阅读4分钟

在使用kotlin 封装SDK将回调改写为协程以便调用主题中,我们先澄清一下同步 异步 阻塞 非阻塞 的关联区别。

同步、异步:

从 API 形式上来看,同步和异步的区别在于:

  • 同步:函数直接返回结果
  • 异步:调用函数时同时传递一个 Callback,或者函数返回的一个 Feature

阻塞、非阻塞:

一般情况都是由于 IO 操作导致阻塞和非阻塞。你可以站在程序或者线程的角度看待这个问题,但是站在程序的立场,很难解释非阻塞这个词汇,所以我们站在线程这个角度来看。

  • 阻塞:我是一个线程,当程序进行某一个调用时,我被阻塞(绑架)了,我不能去干其他事情,只能等待

  • 非阻塞 :我是一个线程,当程序进行某一个 IO 调用时,我知道他需要一些时间,所以我先去执行其他程序

显然非阻塞要能实现,程序必须是复杂的,必须需要运行时环境提供支持。至于运行时环境如何高效实现非阻塞,则必须和操作系统相互配合。(Java 的 NIO、linux 的 select、poll、epoll )

此刻记在你脑子里的是,是否阻塞是底层系统在 IO 调用时给你的能力,同步和异步只是 API 的形式。

从编程难易程度上来看,同步比异步要简单很多,也和人类思维过程比较接近。从系统性能角度来看,非阻塞效率相当高,在互联网高并发场景下,这尤为重要。

对Java开发者而言,“回调”是再常见不过的概念了。从各种SDK到我们自己开发的代码,处处充满了回调。某个任务需要长时间执行,同时我们希望能在任务完成时得到通知,在函数参数里加上一个回调对象,用以收取结果,是十分常见的解决方案。

  1. 嵌套太多,成为“回调地狱”;
  2. 传入的callback如果是Activity会引起泄露;
  3. 代码阅读起来不直观。

这几个隐患不详述了,接下来看看在Kotlin中如何将异步回调转换为同步请求。

下面看如何将耗时函数的回调写法利用协程转换为挂起函数。

举例一:OkHttp 的网络请求转换为挂起函数

suspend fun <T> Call<T>.await(): T = 
suspendCoroutine { cont ->
    enqueue(object : Callback<T> {
        override fun onResponse(call: Call<T>, response: Response<T>) { 
            if (response.isSuccessful) {
                cont.resume(response.body()!!)
            } else {
                cont.resumeWithException(ErrorResponse(response))
            }
        }
        override fun onFailure(call: Call<T>, t: Throwable) { 
            cont.resumeWithException(t)
        } 
    })
}

上面的await()是一个Call拓展函数调用时,使用 suspendCoroutine{} 将请求挂起,然后执行enqueue将网络请求放入队列中,当请求成功时,耗时操作完成后,通过cont.resume(response.body()!!)来恢复之前的协程。resume传递执行结果,resumeWithExeption传递异常。

调用处写法如下:这里假设Service.loadData()会返回一个Call对象

GlobalScope.launch(Dispatchers.Main) {
    try {
        /**
          这里假设Service.loadData()会返回一个Call<T>对象.
        **/
        val result = Service.loadData().await()

    } catch (e: Exception) {
        userNameView.text = "Get User Error: $e"
    }
}

举例二:支持取消的挂起函数

import kotlinx.coroutines.suspendCancellableCoroutine
import retrofit2.Call
import retrofit2.Callback
import retrofit2.HttpException
import retrofit2.Response
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException

suspend fun <T> Call<T>.await(): T = suspendCancellableCoroutine { continuation ->
    continuation.invokeOnCancellation {
        cancel()
    }

    enqueue(object : Callback<T> {
        override fun onFailure(call: Call<T>, t: Throwable) {
            continuation.resumeWithException(t)
        }

        override fun onResponse(call: Call<T>, response: Response<T>) {
            response.takeIf { it.isSuccessful }?.body()?.also {
                continuation.resume(it)
            } ?: continuation.resumeWithException(HttpException(response))
        }

    })
}

支持挂起函数的取消和不支持取消的差异点在于:

  1. 使用suspendCancellableCoroutine{}
  2. 需要调用Call的取消方法cancel(),也就是被扩展的取消方法,这里的cancel是retrofit中的。

 调用方法,最后可以不加cancelAndJoin(),加上的话会立即取消掉而没有任何效果:

GlobalScope.launch(Dispatchers.Main) {
    try {
        /**
          这里假设Service.loadData()会返回一个Call<T>对象.
        **/
        val result = Service.loadData().await()

    } catch (e: Exception) {
        userNameView.text = "Get User Error: $e"
    }
}.cancelAndJoin()

 

举例三:将耗时计算的回调转化为协程写法

类似的,有个 calcSlowlySync 为耗时方法,改写后如下:

suspend fun calcSlowlySync(inp: Int): Int =
    suspendCoroutine { cont ->
        calcSlowly(inp, object: CalcTaskCallback<Int> {
            override fun onSuccess(result: Int) {
                cont.resume(result)
            }

            override fun onFailure(code: Int, msg: String) {
                cont.resumeWithException(Exception("code=$code, msg=$msg"))
            }
        })
    }

调用写法如下:

CoroutineScope(Dispatchers.Main).launch {
    try {
        val result = calcSlowlySync(100)
        println("result=$result")
    } catch (e: Exception) {
        LogUtils.e(TAG, "result exception: ", e)
    }
}

注意如下几点即可:

1、在耗时方法fun前需要添加suspend关键字,表示挂起函数,标注这里是一个耗时操作;

2、在回调函数成功或者失败时,需要将结果返回出去,利用resume传递执行结果,resumeWithExeption传递异常;

3、在外面使用的地方,需要在协程作用域中调用,例如例子中的CoroutineScope(Dispatchers.Main).launch{}内;

4、异步回调的异常处理改写为同步后是通过try{}catch(){}代码块进行捕捉的:一个异步的请求异常,我们只需要在我们的代码中捕获就可以了,这样做的好处就是,请求的全流程异常都可以在一个 try...catch... 当中捕获,那么我们可以说真正做到了把异步代码变成了同步的写法。