使用 Kotlin 协程提升应用性能

Kotlin 协程 使您能够编写简洁、简化的异步代码,使您的应用保持响应,同时管理长时间运行的任务,例如网络调用或磁盘操作。

本主题详细介绍了 Android 上的协程。如果您不熟悉协程,请务必阅读Android 上的 Kotlin 协程,然后再阅读本主题。

管理长时间运行的任务

协程以常规函数为基础,并添加了两个操作来处理长时间运行的任务。除了invoke(或call)和return之外,协程还添加了suspendresume

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

您只能从其他suspend函数或使用协程构建器(如launch)启动新协程来调用suspend函数。

以下示例显示了假设长时间运行任务的简单协程实现

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()调用,使其超越等效的基于回调的实现。例如,如果一个函数对网络进行了十次调用,您可以告诉Kotlin只切换一次线程,方法是使用一个外部的withContext()。然后,即使网络库多次使用withContext(),它也会停留在同一个调度器上,并避免切换线程。此外,Kotlin 会优化Dispatchers.DefaultDispatchers.IO之间的切换,以尽可能避免线程切换。

启动协程

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

  • launch启动一个新的协程,并且不会将结果返回给调用方。任何被认为是“启动并忘记”的工作都可以使用launch启动。
  • async启动一个新的协程,并允许您使用名为await的挂起函数返回结果。

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

并行分解

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

例如,让我们定义一个coroutineScope,它异步获取两个文档。通过对每个延迟引用调用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传递给launchasync不会有任何影响,因为始终会为新协程分配新的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)
        }
    }
}

其他协程资源

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