Android 上的协程(第一部分):背景介绍

协程要解决的问题是什么?

Kotlin 协程引入了一种新的并发风格,可用于 Android 以简化异步代码。 虽然它们是 Kotlin 1.3 中的新手,但自编程语言出现以来,协程的概念就一直存在。 使用协程探索的第一种语言是 1967 年的 Simula。

在过去几年中,协程越来越受欢迎,现在已经包含在许多流行的编程语言中,例如 Javascript、C#、Python、Ruby 和 Go 等等。 Kotlin 协程基于已用于构建大型应用程序的既定概念。

在 Android 上,协程可以很好地解决两个问题:
  • 长时间运行的任务:需要很长时间执行并会阻塞主线程的任务。
  • 主线程安全 :允许您确保可以从主线程调用任何挂起函数。

让我们深入探讨一下协程如何帮助我们以更简洁的方式构建代码!

长时间运行的任务

获取网页或与 API 交互都涉及发出网络请求。 类似地,从数据库读取或从磁盘加载图像涉及读取文件。 这类事情就是我所说的长时间运行的任务——需要很长时间以至于让你的应用停止并等待它们完成!

与网络请求相比,很难理解现代手机执行代码的速度有多快。 在 Pixel 2 上,单个 CPU 周期只需要不到 0.0000000004 秒,这个数字用人类的术语很难掌握。 但是,如果您将网络请求视为眨眼之间,大约 400 毫秒(0.4 秒),则更容易理解 CPU 的运行速度。 一眨眼,或者有点慢的网络请求,CPU 可以执行超过 10 亿个周期!

在 Android 上,每个应用程序都有一个主线程,负责处理 UI(如绘制视图)和协调用户交互。 如果此线程上发生的工作过多,应用程序似乎会挂起或变慢,从而导致不良的用户体验。 任何长时间运行的任务都应该在不阻塞主线程的情况下完成,这样你的应用程序就不会显示所谓的“卡顿”,比如冻结动画,或者对触摸事件响应缓慢。

为了从主线程执行网络请求,一个常见的模式是回调。 回调提供了一个库的句柄,它可以在将来的某个时间回调到你的代码中。 通过回调,获取 developer.android.com 可能如下所示:

class ViewModel: ViewModel() {
   fun fetchDocs() {
       get("developer.android.com") { result ->
           show(result)
       }
    }
}

即使从主线程调用 get,它也会使用另一个线程来执行网络请求。 然后,一旦从网络获得结果,就会在主线程上调用回调。 这是处理长时间运行任务的好方法,像 Retrofit 这样的库可以帮助您在不阻塞主线程的情况下进行网络请求。

将协程用于长时间运行的任务

协程是一种简化用于管理 fetchDocs 等长时间运行任务的代码的方法。 为了探索协程如何使长时间运行的任务的代码更简单,让我们重写上面的回调示例以使用协程。

// Dispatchers.Main
suspend fun fetchDocs() {
    // Dispatchers.Main
    val result = get("developer.android.com")
    // Dispatchers.Main
    show(result)
}
// look at this in the next section
suspend fun get(url: String) = withContext(Dispatchers.IO){/*...*/}

这段代码不会阻塞主线程吗? 它如何在不等待网络请求和阻塞的情况下从 get 返回结果? 事实证明,协程为 Kotlin 提供了一种执行此代码的方法,并且永远不会阻塞主线程。

协程通过在常规函数的基础上添加两个新操作建立。 除了invoke(或call)和return之外,协程还添加了supendresume

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

这个功能是 Kotlin 通过函数上的 suspend 关键字添加的。 您只能从其他挂起函数调用挂起函数,或者使用像 launch 这样的协程构建器来启动新的协程。

Suspend和resume一起工作以替换回调

在上面的例子中,get 将在启动网络请求之前挂起协程。 函数 get 仍将负责从主线程运行网络请求。 然后,当网络请求完成时,它可以简单地恢复它暂停的协程,而不是调用回调来通知主线程。


动画展示了 Kotlin 如何实现挂起和恢复来替换回调

当主线程上的所有协程都被挂起时,主线程可以自由地做其他工作

即使我们编写了看起来完全像阻塞网络请求的简单的顺序代码,协程也会按照我们想要的方式运行我们的代码,并避免阻塞主线程!

接下来,让我们看看如何使用协程实现主线程安全(main-safety)并探索调度程序(dispatchers)。

协程的主线程安全性

在 Kotlin 协程中,编写良好的挂起函数始终可以安全地从主线程调用。 无论他们做什么,他们都应该始终允许任何线程调用它们。

但是,我们在 Android 应用程序中做的很多事情都太慢了,无法在主线程上发生。 网络请求、解析 JSON、从数据库读取或写入,甚至只是迭代大型列表。 其中任何一个都有可能运行得足够慢,导致用户可见的“卡顿”,并且不应该从主线程运行。

使用 suspend 不会告诉 Kotlin 在后台线程上运行一个函数。 值得明确且经常地说,协程将在主线程上运行。 事实上,在启动协程以响应 UI 事件时使用 Dispatchers.Main.immediate 是一个非常好的主意——这样,如果你最终没有执行需要 main-safety 的长时间运行的任务,结果可以 在下一帧中可供用户使用。

协程会在主线程上运行,挂起不代表后台

如果需要处理一个函数,且这个函数在主线程上执行太耗时,但是又要保证这个函数是主线程安全的,那么您可以让 Kotlin 协程在 Default 或 IO 调度器上执行工作。 在 Kotlin 中,所有协程都必须在调度程序中运行——即使它们在主线程上运行。 协程可以自行挂起,调度程序知道如何恢复它们。

为了指定协程应该在哪里运行,Kotlin 提供了三个可用于线程调度的 Dispatcher。

+-----------------------------------+
|         Dispatchers.Main          |
+-----------------------------------+
| Main thread on Android, interact  |
| with the UI and perform light     |
| work                              |
+-----------------------------------+
| - Calling suspend functions       |
| - Call UI functions               |
| - Updating LiveData               |
+-----------------------------------+

+-----------------------------------+
|          Dispatchers.IO           |
+-----------------------------------+
| Optimized for disk and network IO |
| off the main thread               |
+-----------------------------------+
| - Database*                       |
| - Reading/writing files           |
| - Networking**                    |
+-----------------------------------+

+-----------------------------------+
|        Dispatchers.Default        |
+-----------------------------------+
| Optimized for CPU intensive work  |
| off the main thread               |
+-----------------------------------+
| - Sorting a list                  |
| - Parsing JSON                    |
| - DiffUtils                       |
+-----------------------------------+
  • 如果您使用挂起函数、RxJava 或 LiveData,Room 将自动提供主安全。
  • 网络库(例如 Retrofit 和 Volley)管理自己的线程,并且在与 Kotlin 协程一起使用时不需要在代码中显式处理主安全。

继续上面的例子,让我们使用调度器来定义 get 函数。 在 get 的主体内,您调用 withContext(Dispatchers.IO) 来创建一个将在 IO 调度程序上运行的块。 您放入该块中的任何代码将始终在 IO 调度程序上执行。 由于 withContext 本身是一个挂起函数,它将使用协程来提供主线程安全性

// Dispatchers.Main
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
        /* perform blocking network IO here */
    }
    // Dispatchers.Main

使用协程,您可以进行细粒度控制的线程调度。 因为 withContext 使您可以控制任何代码行在哪个线程上执行,而无需引入回调来返回结果,所以您可以将其应用于非常小的功能,例如从数据库读取或执行网络请求。 因此,一个好的做法是使用 withContext 来确保在任何 Dispatcher 上调用每个函数(包括 Main)都是安全的——这样调用者就不必考虑需要哪个线程来执行该函数。

在这个例子中,fetchDocs 在主线程上执行,但可以安全地调用 get 在后台执行网络请求。 因为协程支持挂起和恢复,一旦 withContext 块完成,主线程上的协程就会恢复并且获取结果。

从主线程(或主线程安全)调用编写良好的挂起函数总是安全的

让每一个挂起函数都是 main-safe 是一个非常好的主意。 如果它做了任何涉及磁盘、网络的事情,甚至只是使用了过多的 CPU,请使用 withContext 使其安全地从主线程调用。 这是基于协程的库(如 Retrofit 和 Room)遵循的模式。 如果您在整个代码库中都遵循这种风格,您的代码将会简单得多,并且可以避免将线程问题与应用程序逻辑混合在一起。 如果始终如一地遵循,协程可以在主线程上自由启动,并使用简单的代码发出网络或数据库请求,同时保证用户不会看到“卡顿”。

withContext 的性能

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

下一步

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

在 Android 平台上,您可以使用协程来处理两个常见问题:
简化处理类似于网络请求、磁盘读取甚至是较大 JSON 数据解析这样的耗时任务;

  1. 简化长时间运行任务的代码,例如从网络、磁盘读取,甚至解析大型 JSON 结果。
  2. 执行精确的 main-safety 以确保您永远不会意外阻塞主线程,而不会使代码难以阅读和编写。

引用自Coroutines on Android (part I): Getting the background

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342