使用Kotlin将回调封装为挂起函数

in 默认分类 with 0 comment

在Kotlin1.3中,协程终于正式发布了,这也表示Kotlin协程的语言支持与 API 已完全稳定,可以放心地在项目中使用啦~

然而,目前支持Kotlin协程的第三方库并不多,较多的库还是使用传统的回调来完成各种异步操作。那么,有没有办法将传统库中的回调封装为Kotlin的挂起函数,从而享受到协程带来的便利呢?

答案当然是肯定的,将回调封装为Kotlin协程中的挂起函数其实很简单,只需要调用Kotlin协程中的suspendCoroutine方法,就可以生成一个可供调用的挂起函数啦!

举个例子,下面是我创建的一个测试函数,该函数会在一秒的延时后生成一个随机数并作为传入该函数的回调的参数:

fun test(callback: (number: Int) -> Unit) {
    thread(name = "test") {
        Thread.sleep(1000)
        callback(Random.nextInt())
    }
}

在正常使用回调的场景下,我们需要传入一个对应的lambda表达式:

fun main() {
    test {
       println("Thread=${Thread.currentThread()},result=$it")
    }
}

该程序运行后,将会打印回调执行的线程及收到的随机数,在我的电脑上的一次运行结果如下:

Thread=Thread[test,5,main],result=-2069838608

可以看到,随机数在test线程中得到了打印

现在,我们使用suspendCoroutine来将其封装为一个挂起函数:

suspend fun coroutineWrap() =
    suspendCoroutine<Int> { continuation: Continuation<Int> ->
        test {
            continuation.resume(it)
        }
    }

可以看到,suspendCoroutine函数使用其实很简单,他接收一个参数为Continuation类型的lambda表达式并返回T,以下是其函数声明:

public suspend inline fun <T> suspendCoroutine(crossinline block: (Continuation<T>) -> Unit): T

suspendCoroutine会挂起当前的协程,在传入suspendCoroutine函数的lambda表达式中,我们可以通过调用Continuation的resumeWith方法返回异步调用的结果来恢复挂起的协程。由于resumeWith方法接收的是一个Result类型的参数,Kotlin中还为Continuation扩展出了两个便利的方法,分别是resume(恢复协程,返回结果),resumeWithException(恢复协程,返回异常)。

现在我们已经将test方法包装为一个挂起函数了,现在我们就在协程域中调用一下它试试:

fun main() {
    GlobalScope.launch{
        println("Thread=${Thread.currentThread()},result=${coroutineWrap()}")
    }

    runBlocking {
        delay(2000)
    }
}

在上述代码中,我使用GlobalScope.launch方法创建了一个协程,为了使test方法有足够的时间生成随机数,我在随后将主线程阻塞了两秒钟。在我的电脑上的一次运行结果如下:

Thread=Thread[DefaultDispatcher-worker-2,5,main],result=-560965533

可以看到,通过调用coruntineWrap方法成功的获得了随机数,不过打印却是在协程所对应的线程中。

借助suspendCoroutine方法,我们可以很简单的将一个异步回调封装为挂起函数,但是我们能发现它的不足,那就是挂起函数作为一个函数,它的返回值只能有一个,并且需要一个确定的类型,这就意味着将异步回调转换为挂起函数更适合回调为一个函数并且参数有且仅有一个的场景(或者其它函数都可以使用异常来代替,另外其实这就是大部分使用回调的场景),如果在异步回调中有多个不同参数类型的函数被使用,那还得将不同类型的结果进行封装返回,此时,使用传统的回调或许更为恰当。

另外,使用suspendCoroutine很可能还会带来线程切换,因为Continuation.resumeWith方法需要将结果返回给挂起的协程,如果resumeWith方法不是在挂起的协程所在线程中运行,那么一次线程切换就不可避免了,因此在性能要求比较高或者对回调执行线程有要求的场合,一定要注意这个问题(可以通过指定协程上下文或者直接使用回调来解决)。