跳至主要內容

使用异步/等待

guodongAndroid大约 5 分钟

请使用 IntelliJ 打开本章的 starter 项目,并导航至 async-await/projects/starter 文件夹然后选择 async_await 项目。

现在,您可能想知道在 Kotlin 协程 API 中 async/await 模式是如何工作的。async 在 Kotlin 中返回一个 Deferred<T>,这与 future 模式非常相似。就像您有一个 Future<T> 一样,Deferred 只是包装了一个幕后对象,但是您可以使用它。当您准备好获取数据时,您必须等待,因为数据可能存在也可能不存在。如果数据已经存在,等待将变成简单的 get,否则,您的代码将被挂起并等待数据到来。

很简单,就像您创建了只有单个容量的 BlockingQueue 实例。然后,在任何时间点,您都可以尝试获取数据或者等待数据并挂起代码。关键区别在于您实际上并没有阻塞线程,而是挂起了代码。现在是时候让您看看它是如何在后台使用协程完成的了。

该模式被称为 async/await 是有原因的 —— 因为要完整实现这个模式需要两个函数调用,一个函数用于准备和包装数据,称为 async , 而另一个函数用于获取并使用数据,其被称为 await。在您开始使用这种方法之前,让我们看看这两个函数的签名是什么。

如果您打开 async 函数的定义,您将看到以下代码:

public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T> {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyDeferredCoroutine(newContext, block) else
        DeferredCoroutine<T>(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

当您调用 async 时,您可以传递一个 CoroutineContext 来将它绑定到某个 JobDispatcher。您也可以传入 CoroutineStart 参数以不同的模式启动它。但是,最重要的是您必须要传入一个 lambda 代码块,该代码块可以访问调用该函数的 CoroutineScope,并且返回一个值,该值将尝试存储在 Deferred 中。它创建一个传入 lambda 代码块来启动的新 DeferredCoroutine 并返回该新的 DeferredCoroutine。您将在下一节中了解有关此协程的更多信息。

这个函数调用本质上将值包装在一个协程中,该协程实现了您稍后将在其上调用 awaitDeferred<T> 接口。当您调用 await 时,该协程将尝试去提供值或者挂起直到值存在。让我们看看 await 的函数签名,看看有没有什么有趣的东西:

/**
 * Awaits for completion of this value without blocking a thread
 * and resumes when deferred computation is complete,
 * returning the resulting value or throwing the
 * corresponding exception if the deferred was cancelled.
 *
 * This suspending function is cancellable.
 * If the [Job] of the current coroutine is cancelled
 * or completed while this suspending function is waiting,
 * this function immediately resumes with [CancellationException].
 *
 * This function can be used in [select] invocation
 * with [onAwait] clause.
 * Use [isCompleted] to check for completion of this
 * deferred value without waiting.
 */
public suspend fun await(): T

函数本身极其简单,但是其背后的思想却是极具深意的。您可以挂起整个函数或协程,然后在数据准备好让它解析,而不是实际上阻塞线程。是时候使用这种方法并将当前阻塞的代码片段迁移到异步了。如果您还没有打开本章中的 starter 项目,在 async-await 项目文件夹下。接下来,打开 AsyncAwait.kt,您应该会看到以下代码:

fun main() {
	val userId = 992
	getUserByIdFromNetwork(userId) { user ->
		println(user)
	}
}

private fun getUserByIdFromNetwork(userId: Int, onUserReady: (User) -> Unit) {
    Thread.sleep(3000)
    onUserReady(User(userId, "Filip", "Babic"))
}

data class User(val id: Int, val name: String, val lastName: String)

上面的代码片段尝试从模拟网络调用中获取一个 User 并打印它。但是,问题在于它会调用 Thread.sleep 暂停线程三秒钟。就像您有一个阻塞的网络调用一样,您必须等待数据返回,然后才能使用它。更进一步的问题是当数据准备好后它使用回调来通知调用方。接下来,您将使用协程 API 中的 asyncawait 来重构它。

getUserByIdFromNetwork 代码更改为以下内容:

private fun getUserByIdFromNetwork(userId: Int) = GlobalScope.async {
	println("Retrieving user from network")
	Thread.sleep(3000)
	User(userId, "Filip", "Babic")
}

确保导入 GlobalScopeasync

import kotlinx.coroutines.*

首先,您已经在函数参数中移除了回调,其次,您将 GlobalScope.async 代码块作为函数的返回值。现在,该函数返回一个值,并且它不依赖回调来返回它。

最后,您还需要修改 main 函数里的代码来使用新版本的 getUserByIdFromNetwork()

fun main() {
    val userId = 992
    val userData = getUserByIdFromNetwork(userId)
    println(userData)
}

现在,如果您运行上面的代码,您将看到与此类似的输出:

DeferredCoroutine{Active}@6eee89b3
Retrieving user from network

这是因为 getUserByIdFromNetwork 现在会返回一个 Deferred<User>。为了获取 User,您必须调用它的 await 函数。但是,await 是一个可挂起函数并且必须在另一个可挂起函数或者协程构建器中调用它,所以您还必须将调用代码包装在 launch 中。更新 main 函数代码:

fun main() {
    val userId = 992
    GlobalScope.launch {
        val userData = getUserByIdFromNetwork(userId)
        println(userData.await())
    }
    Thread.sleep(5000)
}

该代码流程与前面的相似,除了您还调用了 await 来获取 User 并打印它外。现在,您成功将基于回调的代码迁移到 async/await 模式。

现在,回顾上面所有更改以及更改代码的作用:

  • 首先,您从函数中删除了回调,因为您将返回一个值。
  • 其次,您将 async 的返回值作为函数的返回值,且从 lambda 代码块中返回 User
  • 最后,您必须在协程中调用 await ,因为它一个可挂起的函数。

这三个步骤是将代码迁移到 async/await 所需要做的一切。

现在,它是如何工作的暂且不提。如前所述,它创建一个协程并使用 Deferred<T> 来包装值。通过该接口,您可以使用它暴露的 await 函数来访问该值。

一旦您调用了 await 就挂起了函数调用,有效地避免了线程阻塞。然后等待 lambda 代码块执行以使用其内部存储的值。当值准备好后,函数将退出挂起并正常执行。

现在,这里的很多工作都是由 Deferred 类型决定的,所以让我们看看它到底是什么。