Kotlin 使用协程编写高效的并发程序[第一行代码 Kotlin 学习笔记]

使用协程编写高效的并发程序

协程属于 Kotlin 中非常有特色的一项技术,因为大部分编程语言中是没有协程这个概念的。

那么什么是协程呢?它其实和线程是有点类似的,可以简单地将它理解成一种轻量级的线程。要知道,我们之前所学习的线程是非常重量级的,它需要依靠操作系统的调度才能实现不同线程之间的切换。而使用协程却可以仅在编程语言的层面就能实现不同协程之间的切换,从而大大提升了并发编程的运行效率

举一个具体点的例子,比如我们有如下 foo() 和 bar() 两个方法:

fun foo() {
    a()
    b()
    c()
}

fun bar() {
    x()
    y()
    z()
}
复制代码

在没有开启线程的情况下,先后调用 foo() 和 bar() 这两个方法,那么理论上结果一定是 a()、b()、c() 执行完了以后,x()、y()、z() 才能够得到执行。而如果使用了协程,在协程 A 中去调用 foo() 方法,协程 B 中去调用 bar() 方法,虽然它们仍然会运行在同一个线程当中,但是在执行 foo() 方法时随时都有可能被挂起转而去执行 bar() 方法,执行 bar() 方法时也随时都有可能被挂起转而继续执行 foo() 方法,最终的输出结果也就变得不确定了。

可以看出,协程允许我们在单线程模式下模拟多线程编程的效果,代码执行时的挂起与恢复完全是由编程语言来控制的,和操作系统无关。这种特性使得高并发程序的运行效率得到了极大的提升,试想一下,开启 10 万个线程完全是不可想象的事吧?而开启 10 万个协程就是完全可行的,待会我们就会对这个功能进行验证。

现在你已经了解了协程的一些基本概念,那么接下来我们就开始学习 Kotlin 中协程的用法。

协程的基本用法

Kotlin 并没有将协程纳入标准库的 API 当中,而是以依赖库的形式提供的。所以如果我们想要使用协程功能,需要先在 app/build.gradle 文件当中添加如下依赖库:

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.8"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0"
复制代码

创建一个 CoroutinesTest.kt 文件,并定义一个 main() 函数,然后开始我们的协程之旅吧。

首先我们要面临的第一个问题就是,如何开启一个协程?最简单的方式就是使用 Global.launch 函数,如下所示:

fun main() {
    GlobalScope.launch {
        println("codes run in coroutine scope")
    }
}
复制代码

GlobalScope.launch 函数可以创建一个协程的作用域,这样传递给 launch 函数的代码块(Lambda 表达式)就是在协程中运行的了,这里我们只是在代码块中打印了一行日志。那么现在运行 main() 函数,日志能成功打印出来吗?如果你尝试一下,会发现没有任何日志输出。

这是因为,Global.launch 函数每次创建的都是一个顶层协程,这种协程当应用程序运行结束时也会跟着一起结束。刚才的日志之所以无法打印出来,就是因为代码块中的代码还没来得及运行,应用程序就结束了。

要解决这个问题也很简单,我们让程序延迟一段时间再结束就行了,如下所示:

fun main() {
    GlobalScope.launch {
        println("codes run in coroutine scope")
    }

    Thread.sleep(1000)
}
复制代码

这里使用 Thread.sleep() 方法让主线程阻塞 1 秒钟,现在重新运行程序,你会发现日志可以正常打印出来了,如下图所示。

image-20210729150337510.png

可是这种写法还是存在问题,如果代码块中的代码在 1 秒钟之内不能运行结束,那么就会被强制中断。观察如下代码:

fun main() {
    GlobalScope.launch {
        println("codes run in coroutine scope")
        delay(1500)
        println("codes run in coroutine scope finished")
    }

    Thread.sleep(1000)
}
复制代码

我们在代码块中加入了一个 delay() 函数,并在之后又打印了一行日志。delay() 函数可以让当前协程延迟指定时间后再运行,但它和 Thread.sleep() 方法不同。delay() 函数是一个非阻塞式的挂起函数,它只会挂起当前协程,并不会影响其他协程的运行。而 Thread.sleep() 方法会阻塞当前的线程,这样运行在该线程下的所有协程都会被阻塞。注意,delay() 函数只能在协程的作用域或其他挂起函数中调用。

这里我们让协程挂起 1.5 秒,但是主线程却只阻塞了 1 秒,最终会是什么结果呢?重新运行程序,你会发现代码块中新增的一条日志并没有打印出来,因为它还没能来得及运行,应用程序就已经结束了。

那么有没有什么办法能让应用程序在协程中所有代码都运行完了之后再结束呢?当然也是有的,借助 runBlocking 函数就可以实现这个功能:

fun main() {
    runBlocking {
        println("codes run in coroutine scope")
        delay(1500)
        println("codes run in coroutine scope finished")
    }
}
复制代码

runBlocking 函数同样会创建一个协程的作用域,但是它可以保证在协程作用域内的所有代码和子协程没有全部执行完之前一直阻塞当前线程。需要注意的是,runBlocking 函数通常只应该在测试环境下使用,在正式环境中使用容易产生一些性能上的问题。

现在重新运行程序,结果如下图所示。

image-20210729152227928.png

可以看到,两条日志都能够正常打印出来了。

虽说现在我们已经能够让代码在协程中运行了,可是好像并没有体会到什么特别的好处。这是因为目前所有的代码都是运行在同一个协程当中的,而一旦涉及高并发的应用场景,协程相比于线程的优势就能体现出来了。

那么如何才能创建多个协程呢?很简单,使用 launch 函数就可以了,如下所示:

fun main() {
    runBlocking {
        launch {
            println("launch1")
            delay(1000)
            println("launch1 finished")
        }
        launch {
            println("launch2")
            delay(1000)
            println("launch2 finished")
        }
    }
}
复制代码

注意这里的 launch 函数和我们刚才所使用的 GlobalScope.launch 函数不同。首先它必须在协程的作用域中才能调用,其次它会在当前协程的作用域下创建子协程。子协程的特点是如果外层作用域的协程结束了,该作用域下的所有子协程也会一同结束。相比而言,GlobalScope.launch 函数创建的永远是顶层协程,这一点和线程比较像,因为线程也没有层级这一说,永远都是顶层的。

这里我们调用了两次 launch 函数,也就是创建了两个子协程。重新运行程序,结果如下图所示。

image-20210729160156475.png

可以看到,两个子协程中的日志是交替打印的,说明它们确实是像多线程那样并发运行的。然而这两个子协程实际却运行在同一个线程当中,只是由编程语言来决定如何在多个协程之间进行调度,让谁运行,让谁挂起。调度的过程完全不需要操作系统参与,这也就使得协程的并发效率会出奇得高。

那么具体会有多高呢?我们来做下实验就知道了,代码如下所示:

fun main() {
    val start = System.currentTimeMillis()
    runBlocking {
        repeat(100000) {
            launch {
                println(".")
            }
        }
    }
    val end = System.currentTimeMillis()
    println(end - start)
}
复制代码

这里使用 repeat 函数循环创建了 10 万个协程,不过在协程当中并没有进行什么有意义的操作,只是象征性地打印了一个点,然后记录一下整个操作的运行耗时。现在重新运行一下程序,结果如下图所示。

image-20210729160642213.png

可以看到,这里仅仅耗时了 469 毫秒,这足以证明协程有多么高效。试想一下,如果开启的是 10 万个线程,程序或许已经出现 OOM 异常了。

不过,随着 launch 函数中的逻辑越来越复杂,可能你需要将部分代码提取到一个单独的函数中。这个时候就产生了一个问题:我们在 launch 函数中编写的代码是拥有协程作用域的,但是提取到一个单独的函数中就没有协程作用域了,那么我们该如何调用像 delay() 这样的挂起函数呢?

为此 Kotlin 提供了一个 suspend 关键字,使用它可以将任意函数声明成挂起函数,而挂起函数之间都是可以互相调用的,如下所示:

suspend fun printDot() {
    println(".")
    delay(1000)
}
复制代码

这样就可以在 printDot() 函数中调用 delay() 函数了。

但是,suspend 关键字只能将一个函数声明成挂起函数,是无法给它提供协程作用域的。比如你现在尝试在 printDot() 函数中调用 launch 函数,一定是无法调用成功的,因为 launch 函数要求必须在协程作用域当中才能调用。

这个问题可以借助 coroutineScope 函数来解决。coroutineScope 函数也是一个挂起函数,因此可以在任何其他挂起函数中调用。它的特点是会继承外部的协程的作用域并创建一个子协程,借助这个特性,我们就可以给任意挂起函数提供协程作用域了。示例写法如下:

suspend fun printDot() = coroutineScope {
    launch {
        println(".")
        delay(1000)
    }
}
复制代码

可以看到,现在我们就可以在 printDot() 这个挂起函数中调用 launch 函数了。

另外,coroutineScope 函数和 runBlocking 函数还有点类似,它可以保证其作用域内的所有代码和子协程在全部执行完之前,外部的协程会一直被挂起。我们来看如下示例代码:

fun main() {
    runBlocking {
        coroutineScope {
            launch {
                for (i in 1..10) {
                    println(i)
                    delay(1000)
                }
            }
        }
        println("coroutineScope finished")
    }
    println("runBlocking finished")
}
复制代码

这里先使用 runBlocking 函数创建了一个协程作用域,然后调用 coroutineScope 函数创建了一个子协程。在 coroutineScope 的作用域中,我们又调用 launch 函数创建了一个子协程,并通过 for 循环依次打印数字 1 到 10,每次打印间隔一秒钟。最后在 runBlocking 和 coroutineScope 函数的结尾,分别又打印了一行日志。现在重新运行一下程序,结果如下图所示。

image-20210730174319797.png

你会看到,控制台会以 1 秒钟的间隔依次输出数字 1 到 10,然后才会打印 coroutineScope 函数结尾的日志,最后打印 runBlocking 函数结尾的日志。

由此可见,coroutineScope 函数确实是将外部协程挂起了,只有当它作用域内的所有代码和子协程都执行完毕之后,coroutineScope 函数之后的代码才能得到运行。

虽然看上去 coroutineScope 函数和 runBlocking 函数的作用是有点类似的,但是 coroutineScope 函数只会阻塞当前协程,既不影响其他协程,也不影响任何线程,因此是不会造成任何性能上的问题的。而 runBlocking 函数由于会挂起外部线程,如果你恰好又在主线程中当中调用它的话,那么就有可能会导致界面卡死的情况,所以不太推荐在实际项目中使用。

好了,现在我们就将协程的基本用法都学习完了,你也算是已经成功入门了。那么接下来,就让我们开始学习协程更多的知识吧。

更多的作用域构建器

在上一小节中,我们学习了 GlobalScope.launch、runBlocking、launch、coroutineScope 这几种作用域构建器,它们都可以用于创建一个新的协程作用域。不过 GlobalScope.launch 和 runBlocking 函数是可以在任意地方调用的,coroutineScope 函数可以在协程作用域或挂起函数中调用,而 launch 函数只能在协程作用域中调用。

前面已经说了,runBlocking 由于会阻塞线程,因此只建议在测试环境下使用。而GlobalScope.launch 由于每次创建的都是顶层协程,一般也不太建议使用,除非你非常明确就是要创建顶层协程。

为什么说不太建议使用顶层协程呢?主要还是因为它管理起来成本太高了。举个例子,比如我们在某个 Activity 中使用协程发起了一条网络请求,由于网络请求是耗时的,用户在服务器还没来得及响应的情况下就关闭了当前 Activity,此时按理说应该取消这条网络请求,或者至少不应该进行回调,因为 Activity 已经不存在了,回调了也没有意义。

那么协程要怎样取消呢?不管是 GlobalScope.launch 函数还是 launch 函数,它们都会返回一个 Job 对象,只需要调用 Job 对象的 cancel() 方法就可以取消协程了,如下所示:

val job = GlobalScope.lanch {
	// 处理具体的逻辑
}
job.cancel()
复制代码

但是如果我们每次创建的都是顶层协程,那么当 Activity 关闭时,就需要逐个调用所有已创建协程的 cancel() 方法,试想一下,这样的代码是不是根本无法维护?

因此,GlobalScope.launch 这种协程作用域构建器,在实际项目中也是不太常用的。下面我来演示一下实际项目中比较常用的写法:

val job = Job()
val scope = CoroutineScope(job)
scope.lanch {
	// 处理具体的逻辑
}
job.cancel()
复制代码

可以看到,我们先创建了一个 Job 对象,然后把它传入 CoroutineScope() 函数当中,注意这里的 CoroutineScope() 是个函数,虽然它的命名更像是一个类。CoroutineScope() 函数会返回一个 CoroutineScope 对象,这种语法结构的设计更像是我们创建了一个 CoroutineScope 的实例,可能也是 Kotlin 有意为之的。有了 CoroutineScope 对象之后,就可以随时调用它的 launch 函数来创建一个协程了。

现在所有调用 CoroutineScope 的 launch 函数所创建的协程,都会被关联在 Job 对象的作用域下面。这样只需要调用一次 cancel() 方法,就可以将同一作用域内的所有协程全部取消,从而大大降低了协程管理的成本。

不过相比之下,CoroutineScope() 函数更适合用于实际项目当中,如果只是在 main() 函数中编写一些学习测试用的代码,还是使用 runBlocking 函数最为方便。

协程的内容确实比较多,下面我们还要继续学习。你已经知道了调用 launch 函数可以创建一个新的协程,但是 launch 函数只能用于执行一段逻辑,却不能获取执行的结果,因为它的返回值永远是一个 Job 对象。那么有没有什么办法能够创建一个协程并获取它的执行结果呢?当然有,使用 async 函数就可以实现。

async 函数必须在协程作用域当中才能调用,它会创建一个新的子协程并返回一个 Deferred 对象,如果我们想要获取 async 函数代码块的执行结果,只需要调用 Deferred 对象的 await() 方法即可,代码如下所示:

fun main() {
    runBlocking {
        val result = async {
            5 + 5
        }.await()
        println(result)
    }
}
复制代码

这里我们在 async 函数的代码块中进行了一个简单的数学运算,然后调用 await() 方法获取运算结果,最终将结果打印出来。重新运行一下代码,结果如下图所示。

image-20210802154550838.png

不过 async 函数的奥秘还不止于此。事实上,在调用了 async 函数之后,代码块中的代码就会立刻开始执行。当调用 await() 方法时,如果代码块中的代码还没执行完,那么 await() 方法会将当前协程阻塞住,直到可以获得 async 函数的执行结果。

为了证实这一点,我们编写如下代码进行验证:

fun main() {
    runBlocking {
        val start = System.currentTimeMillis()
        val result1 = async {
            delay(1000)
            5 + 5
        }.await()

        val result2 = async {
            delay(1000)
            4 + 6
        }.await()

        println("result is ${result1 + result2}.")
        val end = System.currentTimeMillis()
        println("cost ${end - start} ms.")
    }
}
复制代码

这里连续使用了两个 async 函数来执行任务,并在代码块中调用 delay() 方法进行 1 秒的延迟。按照刚才的理论,await() 方法在 async 函数代码块中的代码执行完之前会一直将当前协程阻塞住,那么为了便于验证,我们记录了代码的运行耗时。现在重新运行程序,结果如下图所示。

image-20210802160636951.png

可以看到,整段代码的运行耗时是 2022 毫秒,说明这里的两个 async 函数确实是一种串行的关系,前一个执行完了后一个才能执行。

但是这种写法明显是非常低效的,因为两个 async 函数完全可以同时执行从而提高运行效率。现在对上述代码使用如下的写法进行修改:

fun main() {
    runBlocking {
        val start = System.currentTimeMillis()
        val deferred1  = async {
            delay(1000)
            5 + 5
        }

        val deferred2  = async {
            delay(1000)
            4 + 6
        }

        println("result is ${deferred1.await() + deferred2.await()}.")
        val end = System.currentTimeMillis()
        println("cost ${end - start} ms.")

    }
}
复制代码

现在我们不在每次调用 async 函数之后就立刻使用 await() 方法获取结果了,而是仅在需要用到 async 函数的执行结果时才调用 await() 方法进行获取,这样两个 async 函数就变成一种并行关系了。重新运行程序,结果如下图所示。

image-20210802162143394.png

可以看到,现在整段代码的运行耗时变成了 1021 毫秒,运行效率的提升显而易见。

最后,我们再来学习一个比较特殊的作用域构建器:withContext() 函数。withContext() 函数是一个挂起函数,大体可以将它理解成 async 函数的一种简化版写法,示例写法如下:

fun main() {
    runBlocking {
        val result = withContext(Dispatchers.Default) {
            5 + 5
        }
        println(result)
    }
}
复制代码

我来解释一下这段代码。调用 withContext() 函数之后,会立即执行代码块中的代码,同时将外部协程挂起。当代码块中的代码全部执行完之后,会将最后一行的执行结果作为 withContext() 函数的返回值返回,因此基本上相当于 val result = async{ 5 + 5}.await() 的写法。唯一不同的是,withContext() 函数强制要求我们指定一个线程参数,关于这个参数我准备好好讲一讲。

你已经知道,协程是一种轻量级的线程的概念,因此很多传统编程情况下需要开启多线程执行的并发任务,现在只需要在一个线程下开启多个协程来执行就可以了。但是这并不意味着我们就永远不需要开启线程了,比如说 Android 中要求网络请求必须在子线程中进行,即使你开启了协程去执行网络请求,假如它是主线程当中的协程,那么程序仍然会出错。这个时候我们就应该通过线程参数给协程指定一个具体的运行线程。

线程参数主要有以下3种值可选:Dispatchers.Default、Dispatchers.IO 和 Dispatchers.Main。Dispatchers.Default 表示会使用一种默认低并发的线程策略,当你要执行的代码属于计算密集型任务时,开启过高的并发反而可能会影响任务的运行效率,此时就可以使用 Dispatchers.Default。Dispatchers.IO 表示会使用一种较高并发的线程策略,当你要执行的代码大多数时间是在阻塞和等待中,比如说执行网络请求时,为了能够支持更高的并发数量,此时就可以使用 Dispatchers.IO。Dispatchers.Main 则表示不会开启子线程,而是在 Android 主线程中执行代码,但是这个值只能在 Android 项目中使用,纯 Kotlin 程序使用这种类型的线程参数会出现错误。

事实上,在我们刚才所学的协程作用域构建器中,除了 coroutineScope 函数之外,其他所有的函数都是可以指定这样一个线程参数的,只不过 withContext() 函数是强制要求指定的,而其他函数则是可选的。

到目前为止,你已经掌握了协程中最常用的一些用法,并且了解了协程的主要用途就是可以大幅度地提升并发编程的运行效率。但实际上,Kotlin 中的协程还可以对传统回调的写法进行优化,从而让代码变得更加简洁,那么接下来我们就开始学习这部分的内容。

使用协程简化回调的写法

前面我们学习了编程语言的回调机制,并使用这个机制实现了获取异步网络请求数据响应的功能。不知道你有没有发现,回调机制基本上是依靠匿名类来实现的,但是匿名类的写法通常比较烦琐,比如如下代码:

HttpUtil.sendHttpRequest(address, object : HttpCallbackListener {
    override fun onFinish(response: String) {
    	// 得到服务器返回的具体内容
    }
    override fun onError(e: Exception) {
        // 在这里对异常情况进行处理
    }
})
复制代码

在多少个地方发起网络请求,就需要编写多少次这样的匿名类实现。这不禁引起了我们的思考,有没有更加简单一点的写法呢?

在过去,可能确实没有什么更加简单的写法了。不过现在,Kotlin 的协程使我们的这种设想成为了可能,只需要借助 suspendCoroutine 函数就能将传统回调机制的写法大幅简化,下面我们就来具体学习一下。

suspendCoroutine 函数必须在协程作用域或挂起函数中才能调用,它接收一个 Lambda 表达式参数,主要作用是将当前协程立即挂起,然后在一个普通的线程中执行 Lambda 表达式中的代码。 Lambda 表达式的参数列表上会传入一个 Continuation 参数,调用它的 resume() 方法 resumeWithException() 可以让协程恢复执行。

了解了 suspendCoroutine 函数的作用之后,接下来我们就可以借助这个函数来对传统的回调写法进行优化。首先定义一个 request() 函数,代码如下所示:

suspend fun request(address: String): String {
    return suspendCoroutine { continuation ->
        HttpUtil.sendHttpRequest(address, object : HttpCallbackListener {
            override fun onFinish(response: String) {
                continuation.resume(response)
            }
            override fun onError(e: Exception) {
                continuation.resumeWithException(e)
            }
        })
    }
}
复制代码

可以看到,request() 函数是一个挂起函数,并且接收一个 address 参数。在 request() 函数的内部,我们调用了刚刚介绍的 suspendCoroutine 函数,这样当前协程就会被立刻挂起,而 Lambda 表达式中的代码则会在普通线程中执行。接着我们在 Lambda 表达式中调用 HttpUtil.sendHttpRequest() 方法发起网络请求,并通过传统回调的方式监听请求结果。如果请求成功就调用 Continuation 的 resume() 方法恢复被挂起的协程,并传入服务器响应的数据,该值会成为 suspendCoroutine 函数的返回值。如果请求失败,就调用 Continuation 的 resumeWithException() 恢复被挂起的协程,并传入具体的异常原因。

你可能会说,这里不是仍然使用了传统回调的写法吗?代码怎么就变得更加简化了?这是因为,不管之后我们要发起多少次网络请求,都不需要再重复进行回调实现了。比如说获取百度首页的响应数据,就可以这样写:

suspend fun getBaiduResponse() {
    try {
    	val response = request("https://www.baidu.com/")
    	// 对服务器响应的数据进行处理
    } catch (e: Exception) {
   		// 对异常情况进行处理
}
复制代码

怎么样,有没有觉得代码变得清爽了很多呢?由于 getBaiduResponse() 是一个挂起函数,因此当它调用了 request() 函数时,当前的协程就会被立刻挂起,然后一直等待网络请求成功或失败后,当前协程才能恢复运行。这样即使不使用回调的写法,我们也能够获得异步网络请求的响应数据,而如果请求失败,则会直接进入 catch 语句当中。

不过这里你可能又会产生新的疑惑,getBaiduResponse() 函数被声明成了挂起函数,这样它也只能在协程作用域或其他挂起函数中调用了,使用起来是不是非常有局限性?确实如此,因为 suspendCoroutine 函数本身就是要结合协程一起使用的。不过通过合理的项目架构设计,我们可以轻松地将各种协程的代码应用到一个普通的项目当中,在接下来的项目实战环节你将会学到这部分知识。

事实上,suspendCoroutine 函数几乎可以用于简化任何回调的写法,比如之前使用 Retrofit 来发起网络请求需要这样写:

val appService = ServiceCreator.create<AppService>()
appService.getAppData().enqueue(object : Callback<List<App>> {
    override fun onResponse(call: Call<List<App>>, response: Response<List<App>>) {
    	// 得到服务器返回的数据
    }
    override fun onFailure(call: Call<List<App>>, t: Throwable) {
    	// 在这里对异常情况进行处理
    }
})
复制代码

有没有觉得这里回调的写法也是相当烦琐的?不用担心,使用 suspendCoroutine 函数,我们马上就能对上述写法进行大幅度的简化。

由于不同的 Service 接口返回的数据类型也不同,所以这次我们不能像刚才那样针对具体的类型进行编程了,而是要使用泛型的方式。定义一个 await() 函数,代码如下所示:

suspend fun <T> Call<T>.await(): T {
    return suspendCoroutine { continuation -> 
        enqueue(object : Callback<T> {
            override fun onResponse(call: Call<T>, response: Response<T>) {
                val body = response.body()
                if (body != null) continuation.resume(body)
                else continuation.resumeWithException(
                    RuntimeException("response body is null"))
                }
            override fun onFailure(call: Call<T>, t: Throwable) {
                continuation.resumeWithException(t)
            }
        })
    }
}
复制代码

这段代码相比于刚才的 request() 函数又复杂了一点。首先 await() 函数仍然是一个挂起函数,然后我们给它声明了一个泛型 T,并将 await() 函数定义成了 Call 的扩展函数,这样所有返回值是 Call 类型的 Retrofit 网络请求接口就都可以直接调用 await() 函数了。

接着,await() 函数中使用了 suspendCoroutine 函数来挂起当前协程,并且由于扩展函数的原因,我们现在拥有了 Call 对象的上下文,那么这里就可以直接调用 enqueue() 方法让 Retrofit 发起网络请求。接下来,使用同样的方式对 Retrofit 响应的数据或者网络请求失败的情况进行处理就可以了。另外还有一点需要注意,在 onResponse() 回调当中,我们调用 body() 方法解析出来的对象是可能为空的。如果为空的话,这里的做法是手动抛出一个异常,你也可以根据自己的逻辑进行更加合适的处理。

有了 await() 函数之后,我们调用所有 Retrofit 的 Service 接口都会变得极其简单,比如刚才同样的功能就可以使用如下写法进行实现:

suspend fun getAppData() {
    try {
    	val appList = ServiceCreator.create<AppService>().getAppData().await()
    	// 对服务器响应的数据进行处理
    } catch (e: Exception) {
    	// 对异常情况进行处理
    }
}
复制代码

没有了冗长的匿名类实现,只需要简单调用一下 await() 函数就可以让 Retrofit 发起网络请求,并直接获得服务器响应的数据,有没有觉得代码变得极其简单?当然你可能会觉得,每次发起网络请求都要进行一次 try catch 处理也比较麻烦,其实这里我们也可以选择不处理。在不处理的情况下,如果发生了异常就会一层层向上抛出,一直到被某一层的函数处理了为止。因此,我们也可以在某个统一的入口函数中只进行一次 try catch,从而让代码变得更加精简。

关于 Kotlin 的协程,你已经掌握了足够多的理论知识,下一步就是将它应用到实际的 Android 项目当中了。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享