这是《Kotlin 协程基础课》的第4篇,也是最后一篇文章。

我不在意十年后的自己是什么样子,我在意的是,十年后的我怎么看现在的自己。

协程是强大的工具,通过前面三篇文章我们介绍了什么是协程、如何使用协程、协程里的CoroutineContext到底是个什么玩意儿。本文是《Kotlin协程基础课》系列的最后一篇文章,将从原理上进行简要的介绍。

从混合开发说起

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

一个回调Demo

我们假设有个耗时计算任务,sleep指定时间然后返回数值,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 耗时函数,将运算结果在回调中返回
// 只能在工作线程里调用
fun calcSlowly(inp: Int, callback: CalcTaskCallback<Int>) {
try {
println("calcSlowly inp=$inp in thread: ${Thread.currentThread().name}")
val result = inp * 1000L
Thread.sleep(result)
callback.onSuccess(result.toInt())
} catch (e: Exception) {
println("Fail to calc")
callback.onFailure(-1, "Fail")
}
}

为了获取计算的结果,我们传入了callback参数,用以接收计算结果,或者异常信息。

调用它的代码样例如下,用一个匿名内部对象接收回调:

1
2
3
4
5
6
7
8
9
10
11
12
13
fun execCalcTaskAsync() {
Thread(Runnable {
calcSlowly(3, object : CalcTaskCallback<Int> {
override fun onSuccess(result: Int) {
println("onSuccess, result=$result, in thread: ${Thread.currentThread().name}")
}

override fun onFailure(code: Int, msg: String) {
println("onFailure, code=$code, msg=$msg, in thread: ${Thread.currentThread().name}")
}
})
}).start()
}

这种回调写法在Java中再常见不过了。但是它却有着不小的隐患:

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

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

将异步转化为同步

通过协程的suspendCoroutine关键字,可以将异步回调转换为同步调用,上例改写方法如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
// 包装异步为同步
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"))
}
})
}

改写之后,就可以在协程内部愉快地使用这个同步方法进行耗时计算了。

1
2
3
4
launch(Dispatchers.DEFAULT) {
val result = calcSlowlySync(100)
println("result=$result")
}

suspendCoroutine是如何工作的

todo


原始代码MainCoroutine.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fun main(args: Array<String>) {
runBlocking {
val job = GlobalScope.launch {
val result = calcSlowly(123)
println("result = $result")
}
job.join()
}
print("FINISH")
}

// 延时计算
private suspend fun calcSlowly(inp: Int): Int = withContext(Dispatchers.Default) {
delay(1000L)
inp * 10
}

反编译后生成4个文件

  1. MainCoroutineKt.class,
  2. MainCoroutineKt$main$1.class
  3. MainCoroutineKt$main$1$job$1.class
  4. MainCoroutineKt$calcSlowly$2.class

大纲

混合开发,从把异步请求转换为同步说起

异步接口

转换同步

协程内部如何将异步转换为同步

suspendCoroutine

todo