1. 概述
挂起函数是Kotlin协程最重要的一个特性,所有其他概念都建立在它的基础上。所以我们需要深入了解它的工作原理。
挂起协程意味着在中间停止它。这类似于玩游戏,当我们想暂停游戏时,可以先存档,想继续游戏时,可以从存档处恢复游戏。当协程被暂停时,它会返回一个Continuation. 这就像游戏中的存档,协程可以使用Continuation从挂起的地方恢复。
请注意,这与线程非常不同,线程无法保存,只能阻塞。协程要强大得多,挂起时,它不消耗任何所在线程的资源。协程不一定需要在启动协程的线程上恢复,可以切换到不同的线程上。
2. 恢复
让我们看看它的实际效果。我们使用协程构建器(如runBlocking或launch)启动协程。suspend函数是可以挂起协程的函数。这意味着,它们必须在协程(或另一个挂起函数)上调用。
这是一个简单的程序,将打印“Before”和“After”。如果我们在中间调用协程标准库提供的suspendCoroutine函数会怎样呢?
如果你执行上面的代码,你将发现不会打印“After”,并且main函数不会退出。协程在“Before”之后暂停。我们的程序停止了,再也没有恢复。那我们怎么做呢?我们可以使用Continuation来恢复程序?
我们观察下suspendCoroutine调用,注意它以 lambda 表达式 ( { })结尾。作为参数传递的函数将在暂停之前被调用。此函数将Continuation作为参数。
观察打印日志,我们发现“After”仍然没有打印。我们可以用Continuation的resume方法让协程恢复执行。
请注意,上面示例中的“After”被打印出来,因为我们在suspendCoroutine中调用resume函数。
从 Kotlin 1.3 开始, Continuation的定义发生了变化。resumeWith函数代替了resume和resumeWithException函数。我们使用的resume和resumeWithException是标准库中的扩展函数。
我们还可以使用它来启动一个不同的线程,该线程将休眠一段时间后恢复:
我们还可以将启动线程的代码提取到一个函数中。
这种机制是有效的,但是我们也可以用线程池来代替线程。
暂停一段时间似乎是一个有用的功能。让我们把它提取成一个函数。我们将命名它delay。
这正是Kotlin协程库delay方法曾经的实现方式。目前的实现比较复杂,主要是为了支持测试,但是本质思路还是一样的。
3. 带值恢复
您可能需要注意的是我们为什么将Unit传递给resume函数,为什么我们将Unit作为suspendCoroutine函数泛型类型。Unit是suspendCoroutine函数的返回类型,也是Continuation的泛型类型。
当我们调用suspendCoroutine时,我们可以指定Continuation中的返回类型。resume函数需要调用与之相同的类型。
挂起对协程非常有意义。当我们做耗时操作时,我们需要被挂起。例如,当我们需要从API获取网络响应时,如果没有协程,这个线程就需要等待。因为线程很昂贵,这将是一个巨大的浪费。特别是当这是一个重要的线程,比如 Android 上的主线程。使用协程,它只是挂起协程,然后线程可以去做其他事情。一旦数据到了,线程将从协程挂起点恢复。
举个例子,我们模拟网络请求用户信息:
直接在main方法中调用suspendCoroutine不太方便。我们可以把它提取成一个方法。
目前,许多流行的库如Retrofit或Room已经支持suspend函数。这就是为什么我们很少需要在suspend函数中使用回调函数的原因。还有一个和suspendCoroutine类似的方法suspendCancellableCoroutine,我推荐你使用后者,后者支持取消功能。
你可能想知道如果API不给我们数据而是返回异常会怎样。如果服务死机或响应错误会怎样?。在这种情况下,我们不能返回数据,而是应该从协程挂起的地方抛出异常
4. 异常恢复
我们调用的每个函数都可能返回某个值或抛出异常。对于suspendCoroutine也是如此。我们可以调用resumeWithException从异常处返回。可以用try catch捕获异常。
5. 挂起一个协程,而不是一个函数
需要强调的一件事是我们只是暂停了一个协程,而不是一个函数。想象一下,我们将Continuation存储在某个变量中,并尝试在函数调用后恢复它。
这没有任何意义。resume永远不会被调用。