利用 Kotlin 协程提升应用性能

协程是一种并发设计模式,您可以在 Android 平台上使用它来简化异步执行的代码。协程是在版本 1.3 中添加到 Kotlin 的,它基于来自其他语言的既定概念。

在 Android 平台上,协程有助于解决两个主要问题:

  • 管理长时间运行的任务,如果管理不当,这些任务可能会阻塞主线程并导致您的应用冻结。
  • 提供主线程安全性,或者从主线程安全地调用网络或磁盘操作。

本主题介绍如何使用 Kotlin 协程来解决这些问题,从而让您能够编写出更清晰且更简洁的应用代码。

管理长时间运行的任务

在 Android 平台上,每个应用都有一个用于处理界面并管理用户交互的主线程。如果您的应用为主线程分配的工作过多,可能会看似冻结或运行速度明显变慢。网络请求、JSON 解析、从数据库中读取或写入,甚至只是遍历大型列表就可能会导致您的应用运行非常缓慢,以致出现明显的卡顿 - 界面呈现速度缓慢或界面冻结,对触摸事件的响应速度很慢。这些长时间运行的操作应在主线程之外运行。

以下示例展示了一项任务(假设这是一项长时间运行的任务)的简单协程实现:

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) { /* ... */ }
    

协程在常规函数的基础上添加了两项操作,用于处理长时间运行的任务。在 invoke(或 call)和 return 之外,协程添加了 suspendresume

  • suspend 用于暂停执行当前协程,并保存所有局部变量。
  • resume 用于让已暂停的协程从其暂停处继续执行。

要调用 suspend 函数,您只能从其他 suspend 函数进行调用,或通过使用协程构建器(如 launch)来启动新的协程

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

Kotlin 使用堆栈帧来管理要运行哪个函数以及所有局部变量。暂停协程时,会复制并保存当前的堆栈帧以供稍后使用。恢复时,会将堆栈帧从其保存位置复制回来,然后函数再次开始运行。即使代码可能看起来像普通的顺序阻塞请求,协程也能确保网络请求避免阻塞主线程。

使用协程确保主线程安全

Kotlin 协程使用调度程序来确定哪些线程用于执行协程。要在主线程之外运行代码,您可以让 Kotlin 协程在 Default 或 IO 调度程序上执行工作。在 Kotlin 中,所有协程都必须在调度程序中运行,即使它们在主线程上运行也是如此。协程可以自行暂停,而调度程序负责将其恢复。

Kotlin 提供了三个调度程序,您可以使用它们来指定应在何处运行协程:

  • Dispatchers.Main - 使用此调度程序可在 Android 主线程上运行协程。此调度程序只能用于与界面交互和执行快速工作。示例包括调用 suspend 函数、运行 Android 界面框架操作,以及更新 LiveData 对象。
  • Dispatchers.IO - 此调度程序经过了专门优化,适合在主线程之外执行磁盘或网络 I/O。示例包括使用 Room 组件、从文件中读取数据或向文件中写入数据,以及运行任何网络操作。
  • Dispatchers.Default - 此调度程序经过了专门优化,适合在主线程之外执行占用大量 CPU 资源的工作。用法示例包括对列表排序和解析 JSON。

接着前面的示例来讲,您可以使用调度程序来重新定义 get 函数。在 get 的主体内,调用 withContext(Dispatchers.IO) 来创建一个在 IO 线程池中运行的块。您放在该块内的任何代码都始终通过 IO 调度程序执行。由于 withContext 本身就是一个暂停函数,因此函数 get 也是一个暂停函数。

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 之间的切换,以尽可能避免线程切换。

指定 CoroutineScope

在定义协程时,您还必须指定其 CoroutineScopeCoroutineScope 可管理一个或多个相关的协程。您还可以使用 CoroutineScope 在该范围内启动新的协程。不过,与调度程序不同,CoroutineScope 不运行协程。

CoroutineScope 的一项重要功能就是在用户离开您应用中的内容区域时停止执行协程。使用 CoroutineScope,您可以确保所有正在运行的操作都能正确停止。

将 CoroutineScope 与 Android 架构组件一起使用

在 Android 平台上,您可以将 CoroutineScope 实现与组件生命周期相关联。这样可让您避免泄漏内存或者对不再与用户相关的 Activity 或 Fragment 执行额外的工作。使用 Jetpack 组件,它们自然而然适合放在 ViewModel 中。由于 ViewModel 在配置更改(如屏幕旋转)期间不会被销毁,因此您不必担心协程会被取消或重新启动。

范围知道它们启动的每个协程。这意味着,您可以随时取消在相应范围内启动的一切。范围可以自行传播,因此,如果某个协程启动了另一个协程,则这两个协程的范围相同。这意味着,即使其他库从您的范围启动某个协程,您也可以随时将其取消。如果您在 ViewModel 中运行协程,这一点尤为重要。如果由于用户离开了屏幕而导致您的 ViewModel 被销毁,则必须停止它执行的所有异步工作。否则,您会浪费资源,并且可能会泄漏内存。如果您的某项异步工作在您销毁 ViewModel 后应继续执行,则应在您应用架构的较低层级中完成该工作。

借助 Android 架构组件的 KTX 库,您还可以使用扩展属性 viewModelScope 来创建可以在 ViewModel 被销毁之前运行的协程。

启动协程

您可以通过以下两种方式来启动协程:

  • launch 可启动新协程而不将结果返回给调用方。任何被视为“一劳永逸”的工作都可以使用 launch 来启动。
  • async 可启动新协程并允许您使用一个名为 await 的暂停函数返回 result

通常,您应使用 launch 从常规函数启动新协程,因为常规函数无法调用 await。只有在另一个协程内或在暂停函数内且在执行并行分解时,才使用 async

以前面的示例为基础,下面显示了一个具有 viewModelScope KTX 扩展属性的协程,它使用 launch 从常规函数切换到协程:

fun onDocsNeeded() {
        viewModelScope.launch {    // Dispatchers.Main
            fetchDocs()            // Dispatchers.Main (suspend function call)
        }
    }
    

并行分解

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 会捕获协程抛出的所有异常,并将其传送回调用方。

如需详细了解并行分解,请参阅编写暂停函数

具有内置支持的架构组件

某些架构组件(包括 ViewModelLifecycle)通过它们自己的 CoroutineScope 成员纳入了对协程的内置支持。

例如,ViewModel 包含内置的 viewModelScope。这提供了一种在 ViewModel 的范围内启动协程的标准方法,如以下示例所示:

class MyViewModel : ViewModel() {

        fun launchDataLoad() {
            viewModelScope.launch {
                sortList()
                // Modify UI
            }
        }

        /**
        * Heavy operation that cannot be done in the Main Thread
        */
        suspend fun sortList() = withContext(Dispatchers.Default) {
            // Heavy work
        }
    }
    

此外,LiveData 还将协程与 liveData 块一起使用:

liveData {
        // runs in its own LiveData-specific scope
    }
    

如需详细了解具有内置协程支持的架构组件,请参阅将 Kotlin 协程与架构组件一起使用

更多信息

如需更多与协程相关的信息,请参阅以下链接: