在 Android 开发中使用协程 | 背景介绍

本文是介绍 Android 协程系列中的第一部分,主要会介绍协程是如何工做的,它们主要解决什么问题。

协程用来解决什么问题?

Kotlin 中的协程提供了一种全新处理并发的方式,您能够在 Android 平台上使用它来简化异步执行的代码。协程是从 Kotlin 1.3 版本开始引入,但这一律念在编程世界诞生的黎明之际就有了,最先使用协程的编程语言能够追溯到 1967 年的 Simula 语言。javascript

在过去几年间,协程这个概念发展势头迅猛,现已经被诸多主流编程语言采用,好比 JavascriptC#PythonRuby 以及 Go 等。Kotlin 的协程是基于来自其余语言的既定概念。html

在 Android 平台上,协程主要用来解决两个问题:java

  1. 处理耗时任务 (Long running tasks),这种任务经常会阻塞住主线程;
  2. 保证主线程安全 (Main-safety) ,即确保安全地从主线程调用任何 suspend 函数。

让咱们来深刻上述问题,看看该如何将协程运用到咱们代码中。python

处理耗时任务

获取网页内容或与远程 API 交互都会涉及到发送网络请求,从数据库里获取数据或者从磁盘中读取图片资源涉及到文件的读取操做。一般咱们把这类操做归类为耗时任务 —— 应用会停下并等待它们处理完成,这会耗费大量时间。android

当今手机处理代码的速度要远快于处理网络请求的速度。以 Pixel 2 为例,单个 CPU 周期耗时低于 0.0000000004 秒,这个数字很难用人类语言来表述,然而,若是将网络请求以 “眨眼间” 来表述,大概是 400 毫秒 (0.4 秒),则更容易理解 CPU 运行速度之快。仅仅是一眨眼的功夫内,或是一个速度比较慢的网络请求处理完的时间内,CPU 就已完成了超过 10 亿次的时钟周期了。git

Android 中的每一个应用都会运行一个主线程,它主要是用来处理 UI (好比进行界面的绘制) 和协调用户交互。若是主线程上须要处理的任务太多,应用运行会变慢,看上去就像是 “卡” 住了,这样是很影响用户体验的。因此想让应用运行上不 “卡”、作到动画可以流畅运行或者可以快速响应用户点击事件,就得让那些耗时的任务不阻塞主线程的运行。github

要作处处理网络请求不会阻塞主线程,一个经常使用的作法就是使用回调。回调就是在以后的某段时间去执行您的回调代码,使用这种方式,请求 developer.android.google.cn 的网站数据的代码就会相似于下面这样:golang

class ViewModel: ViewModel() {
   fun fetchDocs() {
       get("developer.android.google.cn") { result ->
           show(result)
       }
    }
}
复制代码

在上面示例中,即便 get 是在主线程中调用的,可是它会使用另一个线程来执行网络请求。一旦网络请求返回结果,result 可用后,回调代码就会被主线程调用。这是一个处理耗时任务的好方法,相似于 Retrofit 这样的库就是采用这种方式帮您处理网络请求,并不会阻塞主线程的执行。数据库

使用协程来处理协程任务

使用协程能够简化您的代码来处理相似 fetchDocs 这样的耗时任务。咱们先用协程的方法来重写上面的代码,以此来说解协程是如何处理耗时任务,从而使代码更清晰简洁的。编程

// Dispatchers.Main
suspend fun fetchDocs() {
    // Dispatchers.Main
    val result = get("developer.android.google.cn")
    // Dispatchers.Main
    show(result)
}
// 在接下来的章节中查看这段代码
suspend fun get(url: String) = withContext(Dispatchers.IO){/*...*/}
复制代码

在上面的示例中,您可能会有不少疑问,难道它不会阻塞主线程吗?get 方法是如何作到不等待网络请求和线程阻塞而返回结果的?其实,是 Kotlin 中的协程提供了这种执行代码而不阻塞主线程的方法。

协程在常规函数的基础上新增了两项操做。在invoke (或 call) 和 return 以外,协程新增了 suspendresume:

  • suspend — 也称挂起或暂停,用于暂停执行当前协程,并保存全部局部变量;
  • resume — 用于让已暂停的协程从其暂停处继续执行。

Kotlin 经过新增 suspend 关键词来实现上面这些功能。您只可以在 suspend 函数中调用另外的 suspend 函数,或者经过协程构造器 (如 launch) 来启动新的协程。

搭配使用 suspend 和 resume 来替代回调的使用。

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

上述动画展现了 Kotlin 如何使用 suspend 和 resume 来代替回调
观察上图中 fetchDocs 的执行,就能明白** suspend** 是如何工做的。Kotlin 使用堆栈帧来管理要运行哪一个函数以及全部局部变量。 暂停协程时,会复制并保存当前的堆栈帧以供稍后使用。 恢复协程时,会将堆栈帧从其保存位置复制回来,而后函数再次开始运行。在上面的动画中,当主线程下全部的协程都被暂停,主线程处理屏幕绘制和点击事件时就会毫无压力。因此用上述的 suspend 和 resume 的操做来代替回调看起来十分的清爽。

当主线程下全部的协程都被暂停,主线程处理别的事件时就会毫无压力。

即便代码可能看起来像普通的顺序阻塞请求,协程也能确保网络请求避免阻塞主线程。

接下来,让咱们来看一下协程是如何保证主线程安全 (main-safety),并来探讨一下调度器。

使用协程保证主线程安全

在 Kotlin 的协程中,主线程调用编写良好的 suspend 函数一般是安全的。无论那些 suspend 函数是作什么的,它们都应该容许任何线程调用它们。

可是在咱们的 Android 应用中有不少的事情处理起来太慢,是不该该放在主线程上去作的,好比网络请求、解析 JSON 数据、从数据库中进行读写操做,甚至是遍历比较大的数组。这些会致使执行时间长从而让用户感受很 “卡” 的操做都不该该放在主线程上执行。

使用 suspend 并不意味着告诉 Kotlin 要在后台线程上执行一个函数,这里要强调的是,协程会在主线程上运行。事实上,当要响应一个 UI 事件从而启动一个协程时,使用 Dispatchers.Main.immediate 是一个很是好的选择,这样的话哪怕是最终没有执行须要保证主线程安全的耗时任务,也能够在下一帧中给用户提供可用的执行结果。

协程会在主线程中运行,suspend 并不表明后台执行。

若是须要处理一个函数,且这个函数在主线程上执行太耗时,可是又要保证这个函数是主线程安全的,那么您可让 Kotlin 协程在 Default 或 IO 调度器上执行工做。在 Kotlin 中,全部协程都必须在调度器中运行,即便它们是在主线程上运行也是如此。协程能够自行暂停,而调度器负责将其恢复

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

  • 相似于 Retrofit Volley 这样的网络库会管理它们自身所使用的线程,因此当您在 Kotlin 协程中调用这些库的代码时不须要专门来处理主线程安全这一问题。

接着前面的示例来说,您可使用调度器来从新定义 get 函数。在 get 的主体内,调用 withContext(Dispatchers.IO) 来建立一个在 IO 线程池中运行的块。您放在该块内的任何代码都始终经过 IO 调度器执行。因为 withContext 自己就是一个 suspend 函数,它会使用协程来保证主线程安全。

// Dispatchers.Main
suspend fun fetchDocs() {
    // Dispatchers.Main
    val result = get("developer.android.google.cn")
    // Dispatchers.Main
    show(result)
}
// Dispatchers.Main
suspend fun get(url: String) =
    // Dispatchers.Main
    withContext(Dispatchers.IO) {
        // Dispatchers.IO
    }
    // Dispatchers.Main
复制代码

借助协程,您能够经过精细控制来调度线程。因为 withContext 可以让您在不引入回调的状况下控制任何代码行的线程池,所以您能够将其应用于很是小的函数,如从数据库中读取数据或执行网络请求。一种不错的作法是使用 withContext 来确保每一个函数都是主线程安全的,这意味着,您能够从主线程调用每一个函数。这样,调用方就无需再考虑应该使用哪一个线程来执行函数了。

在这个示例中,fetchDocs 会在主线程中执行,不过,它能够安全地调用 get 来在后台执行网络请求。由于协程支持 suspendresume,因此一旦 withContext 块完成后,主线程上的协程就会恢复继续执行。

主线程调用编写良好的 suspend 函数一般是安全的。

确保每一个 suspend 函数都是主线程安全的是颇有用的。若是某个任务是须要接触到磁盘、网络,甚至只是占用过多的 CPU,那应该使用 withContext 来确保能够安全地从主线程进行调用。这也是相似于 Retrofit 和 Room 这样的代码库所遵循的原则。若是您在写代码的过程当中也遵循这一点,那么您的代码将会变得很是简单,而且不会将线程问题与应用逻辑混杂在一块儿。同时,协程在这个原则下也能够被主线程自由调用,网络请求或数据库操做代码也变得很是简洁,还能确保用户在使用应用的过程当中不会以为 “卡”。

withContext 的性能

withContext 同回调或者是提供主线程安全特性的 RxJava 相比的话,性能是差很少的。在某些状况下,甚至还能够优化 withContext 调用,让它的性能超越基于回调的等效实现。若是某个函数须要对数据库进行 10 次调用,您可使用外部 withContext 来让 Kotlin 只切换一次线程。这样一来,即便数据库的代码库会不断调用 withContext,它也会留在同一调度器并跟随快速路径,以此来保证性能。此外,在 Dispatchers.Default 和 Dispatchers.IO 中进行切换也获得了优化,以尽量避免了线程切换所带来的性能损失。

下一步

本篇文章介绍了使用协程来解决什么样的问题。协程是一个计算机编程语言领域比较古老的概念,但由于它们可以让网络请求的代码比较简洁,从而又开始流行起来。

在 Android 平台上,您可使用协程来处理两个常见问题:

  1. 似于网络请求、磁盘读取甚至是较大 JSON 数据解析这样的耗时任务;
  2. 线程安全,这样能够在不增长代码复杂度和保证代码可读性的前提下作到不会阻塞主线程的执行。

接下来的文章中咱们将继续探讨协程在 Android 中是如何使用的,感兴趣的读者请继续关注。

点击这里利用 Kotlin 协程提高应用性能

相关文章
相关标签/搜索