使用 Kotlin 协程提升应用性能

Kotlin 协程可让您编写简洁、简化的异步代码,在管理网络调用或磁盘操作等耗时任务时,保持应用响应迅速。

本主题详细介绍了 Android 上的协程。如果您不熟悉协程,请务必在阅读本主题之前,先阅读Android 上的 Kotlin 协程

管理耗时任务

协程通过添加两个操作来处理耗时任务,从而在常规函数的基础上进行构建。除了 invoke(或 call)和 return 之外,协程还添加了 suspendresume

  • suspend 会暂停当前协程的执行,并保存所有局部变量。
  • resume 会从暂停的位置继续执行已暂停的协程。

您只能从其他 suspend 函数中调用 suspend 函数,或者使用诸如 launch 等协程构建器启动新的协程。

以下示例展示了假设的耗时任务的简单协程实现

suspend fun fetchDocs() {                             // Dispatchers.Main
    val result = get("https://developer.android.com") // Dispatchers.IO for `get`
    show(result)                                      // Dispatchers.Main
}

suspend fun get(url: String) = withContext(Dispatchers.IO) { /* ... */ }

在此示例中,get() 仍然在主线程上运行,但在启动网络请求之前会暂停协程。当网络请求完成后,get 会恢复已暂停的协程,而不是使用回调来通知主线程。

Kotlin 使用堆栈帧来管理正在运行的函数以及所有局部变量。暂停协程时,当前的堆栈帧会被复制并保存以备后用。恢复时,堆栈帧会从保存的位置复制回来,函数会再次开始运行。尽管代码可能看起来像普通的顺序阻塞请求,但协程可确保网络请求不会阻塞主线程。

使用协程实现主线程安全

Kotlin 协程使用调度器来确定协程执行所使用的线程。要在主线程之外运行代码,您可以指示 Kotlin 协程在DefaultIO 调度器上执行工作。在 Kotlin 中,所有协程都必须在调度器中运行,即使它们在主线程上运行也是如此。协程可以自行暂停,调度器负责恢复它们。

为指定协程的运行位置,Kotlin 提供了以下三个调度器供您使用

  • Dispatchers.Main - 使用此调度器在 Android 主线程上运行协程。此调度器仅应用于与 UI 交互和执行快速工作。示例包括调用 suspend 函数、运行 Android UI 框架操作以及更新 LiveData 对象。
  • Dispatchers.IO - 此调度器经过优化,可在主线程之外执行磁盘或网络 I/O。示例包括使用 Room 组件、读写文件以及运行任何网络操作。
  • Dispatchers.Default - 此调度器经过优化,可在主线程之外执行 CPU 密集型工作。示例用例包括对列表进行排序和解析 JSON。

继续上一个示例,您可以使用调度器重新定义 get 函数。在 get 的函数体内,调用 withContext(Dispatchers.IO) 以创建在 IO 线程池中运行的代码块。您放在该块中的任何代码都始终通过 IO 调度器执行。由于 withContext 本身是一个 suspend 函数,因此 get 函数也是一个 suspend 函数。

suspend fun fetchDocs() {                      // Dispatchers.Main
    val result = get("developer.android.com")  // Dispatchers.Main
    show(result)                               // Dispatchers.Main
}

suspend fun get(url: String) =                 // Dispatchers.Main
    withContext(Dispatchers.IO) {              // Dispatchers.IO (main-safety block)
        /* perform network IO here */          // Dispatchers.IO (main-safety block)
    }                                          // Dispatchers.Main
}

借助协程,您可以精细地控制线程调度。由于 withContext() 可让您在不引入回调的情况下控制任何一行代码的线程池,因此您可以将其应用于非常小的函数,例如从数据库读取数据或执行网络请求。一个好的做法是使用 withContext() 来确保每个函数都是主线程安全的,这意味着您可以从主线程调用该函数。这样,调用方无需考虑应该使用哪个线程来执行该函数。

在前面的示例中,fetchDocs() 在主线程上执行;但是,它可以安全地调用 get,后者会在后台执行网络请求。由于协程支持 suspendresume,因此一旦 withContext 块完成,主线程上的协程就会以 get 结果恢复。

withContext() 的性能

withContext() 与等效的基于回调的实现相比,不会增加额外的开销。此外,在某些情况下,可以对 withContext() 调用进行优化,使其超出等效的基于回调的实现。例如,如果一个函数对网络进行十次调用,您可以通过使用外部 withContext() 来告诉 Kotlin 只切换一次线程。然后,即使网络库多次使用 withContext(),它也会停留在同一个调度器上并避免切换线程。此外,Kotlin 优化了 Dispatchers.DefaultDispatchers.IO 之间的切换,尽可能避免线程切换。

启动协程

您可以通过以下两种方式之一启动协程

  • launch 启动一个新的协程,并且不将结果返回给调用方。任何被认为是“即发即弃”的工作都可以使用 launch 来启动。
  • async 启动一个新的协程,并允许您通过一个名为 await 的 suspend 函数返回结果。

通常,您应该从常规函数中 launch 一个新的协程,因为常规函数无法调用 await。仅当在另一个协程内部或在 suspend 函数内部执行并行分解时才使用 async

并行分解

suspend 函数内启动的所有协程都必须在该函数返回时停止,因此您可能需要保证这些协程在返回之前完成。借助 Kotlin 中的结构化并发,您可以定义一个 coroutineScope 来启动一个或多个协程。然后,使用 await()(用于单个协程)或 awaitAll()(用于多个协程),您可以保证这些协程在函数返回之前完成。

举例来说,让我们定义一个 coroutineScope 来异步获取两个文档。通过在每个 deferred 引用上调用 await(),我们保证两个 async 操作在返回一个值之前完成

suspend fun fetchTwoDocs() =
    coroutineScope {
        val deferredOne = async { fetchDoc(1) }
        val deferredTwo = async { fetchDoc(2) }
        deferredOne.await()
        deferredTwo.await()
    }

您还可以在集合上使用 awaitAll(),如以下示例所示

suspend fun fetchTwoDocs() =        // called on any Dispatcher (any thread, possibly Main)
    coroutineScope {
        val deferreds = listOf(     // fetch two docs at the same time
            async { fetchDoc(1) },  // async returns a result for the first doc
            async { fetchDoc(2) }   // async returns a result for the second doc
        )
        deferreds.awaitAll()        // use awaitAll to wait for both network requests
    }

尽管 fetchTwoDocs() 使用 async 启动了新的协程,但该函数使用 awaitAll() 来等待这些已启动的协程完成,然后才返回。请注意,即使我们没有调用 awaitAll()coroutineScope 构建器也不会在所有新协程完成之前恢复调用 fetchTwoDocs 的协程。

此外,coroutineScope 会捕获协程抛出的任何异常,并将其路由回调用方。

有关并行分解的更多信息,请参阅组合挂起函数

协程概念

CoroutineScope

一个 CoroutineScope 会跟踪它使用 launchasync 创建的任何协程。正在进行的工作(即正在运行的协程)可以通过随时调用 scope.cancel() 来取消。在 Android 中,一些 KTX 库为特定的生命周期类提供了自己的 CoroutineScope。例如,ViewModel 有一个 viewModelScope,而 LifecyclelifecycleScope。然而,与调度器不同的是,CoroutineScope 不会运行协程。

viewModelScope 也用于Android 上的协程后台线程处理中的示例。但是,如果您需要创建自己的 CoroutineScope 来控制应用特定层中协程的生命周期,可以按如下方式创建:

class ExampleClass {

    // Job and Dispatcher are combined into a CoroutineContext which
    // will be discussed shortly
    val scope = CoroutineScope(Job() + Dispatchers.Main)

    fun exampleMethod() {
        // Starts a new coroutine within the scope
        scope.launch {
            // New coroutine that can call suspend functions
            fetchDocs()
        }
    }

    fun cleanUp() {
        // Cancel the scope to cancel ongoing coroutines work
        scope.cancel()
    }
}

已取消的作用域无法创建更多协程。因此,您应该仅在控制其生命周期的类被销毁时才调用 scope.cancel()。使用 viewModelScope 时,ViewModel 类会在 ViewModel 的 onCleared() 方法中自动为您取消作用域。

Job

一个 Job 是协程的句柄。您使用 launchasync 创建的每个协程都会返回一个 Job 实例,该实例唯一标识协程并管理其生命周期。您还可以将 Job 传递给 CoroutineScope 以进一步管理其生命周期,如以下示例所示

class ExampleClass {
    ...
    fun exampleMethod() {
        // Handle to the coroutine, you can control its lifecycle
        val job = scope.launch {
            // New coroutine
        }

        if (...) {
            // Cancel the coroutine started above, this doesn't affect the scope
            // this coroutine was launched in
            job.cancel()
        }
    }
}

CoroutineContext

一个 CoroutineContext 使用以下元素集定义协程的行为

对于在作用域内创建的新协程,一个新的 Job 实例会被分配给新协程,而其他 CoroutineContext 元素则继承自包含作用域。您可以通过将新的 CoroutineContext 传递给 launchasync 函数来覆盖继承的元素。请注意,将 Job 传递给 launch或 async 没有效果,因为新的协程总是会分配一个新的 Job 实例。

class ExampleClass {
    val scope = CoroutineScope(Job() + Dispatchers.Main)

    fun exampleMethod() {
        // Starts a new coroutine on Dispatchers.Main as it's the scope's default
        val job1 = scope.launch {
            // New coroutine with CoroutineName = "coroutine" (default)
        }

        // Starts a new coroutine on Dispatchers.Default
        val job2 = scope.launch(Dispatchers.Default + CoroutineName("BackgroundCoroutine")) {
            // New coroutine with CoroutineName = "BackgroundCoroutine" (overridden)
        }
    }
}

更多协程资源

有关更多协程资源,请参阅以下链接