跳至主要內容

延迟值

guodongAndroid大约 6 分钟

延迟值

每个 async 代码块都会返回一个 Deferred<T> 。它是协程 API 的核心原理,了解它的工作原理非常重要。

async 会创建一个 DeferredCoroutine 或者一个 LazyDeferredCoroutine。这些协程都有一个公共的特点:实现了 Continuation<T> 接口,允许拦截执行流程并可以将值一直传递到调用处,就像 suspendCoroutine 一样。这看来与之前的 future pattern 的工作方式类似。

一旦协程被创建,除非指定了 CoroutineStartCoroutineStart.LAZY,否则它将会立即启动。代码将在通过 context 参数传入的 Dispatchers 声明的线程中执行。当代码执行完毕并产生一个值后,这个值将被存储在协程内部。如果在任意时刻调用 await,它将会是一起挂断调用,同时它也会创建一个新的续体和执行流程,直到 await 返回值。如果协程还没有准备好值,那么这个函数必须等待。如果协程已经准备好值,那么将会立即得到值。

同时我们还可以检查 deferred value 的状态,因为它也实现了 Job 接口。我们可以使用 isActiveisCompleted 来了解它当前生命周期的状态。我们还可以使用其他的函数来获取协程的值,如果协程被取消,那么将会得到异常。getCompleted 函数可以返回协程的值,或者协程被取消,它将返回一个异常。getCompletionExceptionOrNull 函数可以返回 CancellationException 异常,如何协程没有被取消,它将返回 null。通过上述函数,我们可以了解 deferred value 完成状态的详细信息。

所以,什么是 Deferred 的最好诠释:它是一个带有返回结果的 Job。它可以并行运行,它可能会,也可能不会生成结果值,并且它可以被取消以及连接其它的 Job。它提供了一个强大的 API,我们可以随心所欲的调用。我们可以利用延迟值的特性:将延迟值组合在一个具有多个参数的函数中。

组合多个延迟值

能够将多个在后台构建的延迟值在一个函数调用中,并且可以在主线程访问,这是一件了不起的事情。但是 async 的真正强大之处在于能够将两个或多个延迟值组合到一个函数调用中。让我们看看如何实现。

到目前为止,我们已经学习了一个模拟网络请求的示例,但现在是时候了扩展下该示例了。在项目中有一个 13,000 行文本的名为 users.txt 的文件。大多数行的信息包含有一个 id,一个 name 以及一个 last name,这些信息可以构建 Users 类。有些是空行,还有些行没有包含完整的上述三个信息,甚至有些行是脏数据。我们的想法是读取整个文件,解析和拆分每一行并从中创建用户。在这之后,我们可以得到一个 User 的集合,最后我们检查通过模拟网络请求得到的 User 是否存储在文件中。

通过这个想法,我们就可以看到如何在单个函数调用中启动和使用两个延迟值。现在我们打开 AsyncAwait.kt 。在那里,将以下代码添加到文件中:

private fun readUsersFromFile(filePath: String) =
	GlobalScope.async {
	    println("Reading the file of users")
	    delay(1000)
	    File(filePath)
	    .readLines()
	    .asSequence()
	    .filter { it.isNotEmpty() }
	    .map {
	        val data = it.split(" ") // [id, name, lastName]
	        if (data.size == 3) data else emptyList()
	    }
	    .filter {
	        it.isNotEmpty()
	    }
	    .map {
	        val userId = it[0].toInt()
	        val name = it[1]
	        val lastName = it[2]
	        User(userId, name, lastName)
	    }
	    .toList()
	}

private fun checkUserExists(user: User, users: List<User>): Boolean {
    return user in users
}

别忘记下面的导包:

import java.io.File

此函数以异步方式执行上述所有操作,并将返回一个包含用户列表的 Deferred。 现在,我们可以将 main 函数中的代码调整为如下所示:

fun main() {
    val userId = 992
    GlobalScope.launch {
        println("Finding user")
        val userDeferred = getUserByIdFromNetwork(userId)
        val usersFromFileDeferred = readUsersFromFile("users.txt")
        val userStoredInFile = checkUserExists(
            userDeferred.await(), usersFromFileDeferred.await()
        )
        if (userStoredInFile) {
            println("Found user in file!")
        }
    }
    Thread.sleep(5000)
}

在上面的代码中:

  • 通过调用 getUserByIdFromNetwork 创建了一个 userDeferred 变量。getUserByIdFromNetwork 函数将会延迟 3秒返回一个 User
  • 然后将存储在 users.txt 文件中的用户列表读取到 usersFromFileDeferred 变量。
  • 当上述两个变量都准备好后,就可以调用 checkUserExists 函数来检查从网络获取到的 user 是否存储在文件中。
  • checkUserExists 函数需要两个参数:其中一个是需要被检查的 user,另外一个是检查 user 是否在其中的 User 集合。
  • 要传入上述两个参数值,必须要等待 Deferred 的结果。
  • 最后,如果 user 存储在文件中,那么就输出:“Found user in file!” 。

我们传入 checkUserExists 函数的参数是之前准备的两个延迟值的等待结果,并且只通过一行代码:挂起函数并获取了两个值。

这是使用多个延迟值的正确方法。我们只创建了一个挂起点,但是它挂起了两个函数。另一种方式是把 checkUserExists 变成挂起函数,然后在其内部等待延迟值,但是将延迟值进一步传递给其他函数之前就得到值更加的简单明了。

运行上述代码,我们可以看到以下输出:

Finding user
Retrieving user from network
Reading the file of users

大概 3秒后,我们还可以看到 Found user in file!。这是因为通过 Deferred 创建的协程不是惰性的,它们会立即启动——只有当调用 await 时,才会挂起代码。调用 await 之后,就可以得到 checkUserExists 的返回值,并且输出 user 在文件中存在。

使用这种方法,可以组合任意数量的延迟值并实现智能和简单的并行性,但它不是基于数据流和回调的概念。基于到这一点,代码非常容易理解,因为它类似于顺序的同步代码,即使在背后它可能完全是异步的。这就是协程和异步/等待模式的真正力量。

在上一章中,我们使用 withContext 实现了相同的行为,但是只能返回一个值。这是两种模式之间的主要区别 —— withContext 最适用于需要从一段可挂起的代码中返回单个值的场景,而 async/await 最适用于并行运行的多个函数组合产生一个结果的场景。

然而,这段代码有一点并不理想:事实上,在取消和资源管理方面,这段代码的结构不是很好。让我们看看如何按照 Jetbrain's 的理念将代码打磨至完美。