乐趣区

关于kotlin:在-Android-开发中使用协程-上手指南

本文是介绍 Android 协程系列中的第二局部,这篇文章次要会介绍如何应用协程来解决工作,并且能在工作开始执行后放弃对它的追踪。

放弃对协程的追踪

本系列文章的第一篇,咱们探讨了协程适宜用来解决哪些问题。这里再简略回顾一下,协程适宜解决以下两个常见的编程问题:

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

协程通过在惯例函数之上减少 suspend 和 resume 两个操作来解决上述问题。当某个特定的线程上的所有协程被 suspend 后,该线程便可腾出资源去解决其余工作。

协程本身并不可能追踪正在解决的工作,然而有成千盈百个协程并对它们同时执行挂起操作并没有太大问题。协程是轻量级的,但解决的工作却不肯定是轻量的,比方读取文件或者发送网络申请。

应用代码来手动追踪上千个协程是十分艰难的,您能够尝试对所有协程进行跟踪,手动确保它们都实现了或者都被勾销了,那么代码会臃肿且易出错。如果代码不是很完满,就会失去对协程的追踪,也就是所谓 “work leak” 的状况。

工作透露 (work leak) 是指某个协程失落无奈追踪,它相似于内存透露,但比它更加蹩脚,这样失落的协程能够复原本人,从而占用内存、CPU、磁盘资源,甚至会发动一个网络申请,而这也意味着它所占用的这些资源都无奈失去重用。

透露协程会节约内存、CPU、磁盘资源,甚至发送一个无用的网络申请。

为了可能防止协程透露,Kotlin 引入了 结构化并发 (structured concurrency) 机制,它是一系列编程语言个性和实际指南的联合,遵循它能帮忙您追踪到所有运行于协程中的工作。

在 Android 平台上,咱们能够应用结构化并发来做到以下三件事:

  1. 勾销工作 —— 当某项工作不再须要时勾销它;
  2. 追踪工作 —— 当工作正在执行时,追踪它;
  3. 收回谬误信号 —— 当协程失败时,收回谬误信号表明有谬误产生。

接下来咱们对以上几点一一进行探讨,看看结构化并发是如何帮忙可能追踪所有协程,而不会导致透露呈现的。

借助 scope 来勾销工作

在 Kotlin 中,定义协程必须指定其 CoroutineScope。CoroutineScope 能够对协程进行追踪,即便协程被挂起也是如此。同 第一篇文章 中讲到的调度程序 (Dispatcher) 不同,CoroutineScope 并不运行协程,它只是确保您不会失去对协程的追踪。

为了确保所有的协程都会被追踪,Kotlin 不容许在没有应用 CoroutineScope 的状况下启动新的协程。CoroutineScope 可被看作是一个具备超能力的 ExecutorService 的轻量级版本。它能启动新的协程,同时这个协程还具备咱们在第一局部所说的 suspend 和 resume 的劣势。

CoroutineScope 会跟踪所有协程,同样它还能够勾销由它所启动的所有协程。这在 Android 开发中十分有用,比方它可能在用户来到界面时进行执行协程。

CoroutineScope 会跟踪所有协程,并且能够勾销由它所启动的所有协程。

启动新的协程

须要特地留神的是,您不能轻易就在某个中央调用 suspend 函数,suspend 和 resume 机制要求您从惯例函数中切换到协程。

有两种形式可能启动协程,它们别离实用于不同的场景:

  1. launch 构建器适宜执行 “ 一劳永逸 ” 的工作,意思就是说它能够启动新协程而不将后果返回给调用方;
  2. async 构建器可启动新协程并容许您应用一个名为 await 的挂起函数返回 result。

通常,您应应用 launch 从惯例函数中启动新协程。因为惯例函数无奈调用 await (记住,它无奈间接调用 suspend 函数),所以将 async 作为协程的次要启动办法没有多大意义。稍后咱们会探讨应该如何应用 async。

您应该改为应用 coroutine scope 调用 launch 办法来启动协程。

scope.launch {
    // 这段代码在作用域里启动了一个新协程
   // 它能够调用挂起函数
   fetchDocs()}

您能够将 launch 看作是将代码从惯例函数送往协程世界的桥梁。在 launch 函数体内,您能够调用 suspend 函数并可能像咱们 上一篇 介绍的那样保障主线程平安。

Launch 是将代码从惯例函数送往协程世界的桥梁。

留神: launch 和 async 之间的很大差别是它们对异样的解决形式不同。async 冀望最终是通过调用 await 来获取后果 (或者异样),所以默认状况下它不会抛出异样。这意味着如果应用 async 启动新的协程,它会静默地将异样抛弃。

因为 launch 和 async 仅可能在 CouroutineScope 中应用,所以任何您所创立的协程都会被该 scope 追踪。Kotlin 禁止您创立不可能被追踪的协程,从而防止协程透露。

在 ViewModel 中启动协程

既然 CoroutineScope 会追踪由它启动的所有协程,而 launch 会创立一个新的协程,那么您应该在什么中央调用 launch 并将其放在 scope 中呢? 又该在什么时候勾销在 scope 中启动的所有协程呢?

在 Android 平台上,您能够将 CoroutineScope 实现与用户界面相关联。这样可让您防止透露内存或者对不再与用户相干的 Activities 或 Fragments 执行额定的工作。当用户通过导航来到某界面时,与该界面相干的 CoroutineScope 能够勾销掉所有不须要的工作。

结构化并发可能保障当某个作用域被勾销后,它外部所创立的所有协程也都被勾销。

当将协程同 Android 架构组件 (Android Architecture Components) 集成起来时,您往往会须要在 ViewModel 中启动协程。因为大部分的工作都是在这里开始进行解决的,所以在这个中央启动是一个很正当的做法,您也不必放心旋转屏幕方向会终止您所创立的协程。

从生命周期感知型组件 (AndroidX Lifecycle) 的 2.1.0 版本开始 (公布于 2019 年 9 月),咱们通过增加扩大属性 ViewModel.viewModelScope 在 ViewModel 中退出了协程的反对。

举荐您浏览 Android 开发者文档 “ 将 Kotlin 协程与架构组件一起应用 ” 理解更多。

看看如下示例:

class MyViewModel(): ViewModel() {fun userNeedsDocs() {
       // 在 ViewModel 中启动新的协程
        viewModelScope.launch {fetchDocs()
        }
    }
}

当 viewModelScope 被革除 (当 onCleared() 回调被调用时) 之后,它将主动勾销它所启动的所有协程。这是一个规范做法,如果一个用户在尚未获取到数据时就敞开了利用,这时让申请持续实现就纯正是在节约电量。

为了进步安全性,CoroutineScope 会进行自行流传。也就是说,如果某个协程启动了另一个新的协程,它们都会在同一个 scope 中终止运行。这意味着,即便当某个您所依赖的代码库从您创立的 viewModelScope 中启动某个协程,您也有办法将其勾销。

留神: 协程被挂起时,零碎会以抛出 CancellationException 的形式 合作勾销 协程。捕捉顶级异样 (如 Throwable) 的异样处理程序将捕捉此异样。如果您做异样解决时生产了这个异样,或从未进行 suspend 操作,那么协程将会彷徨于半勾销 (semi-canceled) 状态下。

所以,当您须要将一个协程同 ViewModel 的生命周期保持一致时,应用 viewModelScope 来从惯例函数切换到协程中。而后,viewModelScope 会主动为您勾销协程,因而在这里哪怕是写了死循环也是齐全不会产生透露。如下示例:

fun runForever() {
    // 在 ViewModel 中启动新的协程
    viewModelScope.launch {
        // 当 ViewModel 被革除后,下列代码也会被勾销
        while(true) {delay(1_000)
           // 每过 1 秒做点什么
        }
    }
}

通过应用 viewModelScope,能够确保所有的工作,蕴含死循环在内,都能够在不须要的时候被勾销掉。

工作追踪

应用协程来解决工作对于很多代码来说真的很不便。启动协程,进行网络申请,将后果写入数据库,所有都很天然晦涩。

但有时候,可能会遇到略微简单点的问题,例如您须要在一个协程中同时解决两个网络申请,这种状况下须要启动更多协程。

想要创立多个协程,能够在 suspend function 中应用名为 coroutineScope 或 supervisorScope 这样的结构器来启动多个协程。然而这个 API 说实话,有点令人困惑。coroutineScope 结构器和 CoroutineScope 这两个的区别只是一个字符之差,但它们却是齐全不同的货色。

另外,如果随便启动新协程,可能会导致潜在的工作透露 (work leak)。调用方可能感知不到启用了新的协程,也就意味着无奈对其进行追踪。

为了解决这个问题,结构化并发施展了作用,它保障了当 suspend 函数返回时,就意味着它所解决的工作也都已实现。

结构化并发保障了当 suspend 函数返回时,它所解决工作也都已实现。

示例应用 coroutineScope 来获取两个文档内容:

suspend fun fetchTwoDocs() {
    coroutineScope {launch { fetchDoc(1) }
        async {fetchDoc(2) }
    }
}

在这个示例中,同时从网络中获取两个文档数据,第一个是通过 launch 这样 “ 一劳永逸 ” 的形式启动协程,这意味着它不会返回任何后果给调用方。

第二个是通过 async 的形式获取文档,所以是会有返回值返回的。不过下面示例有一点奇怪,因为通常来讲两个文档的获取都应该应用 async,但这里我仅仅是想举例来说明能够依据须要来抉择应用 launch 还是 async,或者是对两者进行混用。

coroutineScope 和 supervisorScope 能够让您平安地从 suspend 函数中启动协程。

然而请留神,这段代码不会显式地期待所创立的两个协程实现工作后才返回,当 fetchTwoDocs 返回时,协程还正在运行中。

所以,为了做到结构化并发并防止透露的状况产生,咱们想做到在诸如 fetchTwoDocs 这样的 suspend 函数返回时,它们所做的所有工作也都能完结。换个说法就是,fetchTwoDocs 返回之前,它所启动的所有协程也都能实现工作。

Kotlin 确保应用 coroutineScope 结构器不会让 fetchTwoDocs 产生透露,coroutinScope 会先将本身挂起,期待它外部启动的所有协程实现,而后再返回。因而,只有在 coroutineScope 构建器中启动的所有协程实现工作之后,fetchTwoDocs 函数才会返回。

解决一堆工作

既然咱们曾经做到了追踪一两个协程,那么来个刺激的,追踪一千个协程来试试!

先看看上面这个动画:

这个动画展现了 coroutineScope 是如何追踪一千个协程的。

这个动画向咱们展现了如何同时收回一千个网络申请。当然,在实在的 Android 开发中最好别这么做,太浪费资源了。

这段代码中,咱们在 coroutineScope 结构器中应用 launch 启动了一千个协程,您能够看到这所有是如何分割到一起的。因为咱们应用的是 suspend 函数,因而代码肯定应用了 CoroutineScope 创立了协程。咱们目前对这个 CoroutineScope 无所不知,它可能是 viewModelScope 或者是其余中央定义的某个 CoroutineScope,但不管怎样,coroutineScope 结构器都会应用它作为其创立新的 scope 的父级。

而后,在 coroutineScope 代码块内,launch 将会在新的 scope 中启动协程,随着协程的启动实现,scope 会对其进行追踪。最初,一旦所有在 coroutineScope 内启动的协程都实现后,loadLots 办法就能够轻松地返回了。

留神: scope 和协程之间的父子关系是应用 Job 对象进行创立的。然而您不须要深刻去理解,只有晓得这一点就能够了。

coroutineScope 和 supervisorScope 将会期待所有的子协程都实现。

以上的重点是,应用 coroutineScope 和 supervisorScope 能够从任何 suspend function 来平安地启动协程。即便是启动一个新的协程,也不会呈现透露,因为在新的协程实现之前,调用方始终处于挂起状态。

更厉害的是,coroutineScope 将会创立一个子 scope,所以一旦父 scope 被勾销,它会将勾销的消息传递给所有新的协程。如果调用方是 viewModelScope,这一千个协程在用户来到界面后都会主动被勾销掉,十分整洁高效。

在持续探讨报错 (error) 相干的问题之前,有必要花点工夫来讨论一下 supervisorScope 和 coroutineScope,它们的次要区别是当呈现任何一个子 scope 失败的状况,coroutineScope 将会被勾销。如果一个网络申请失败了,所有其余的申请都将被立刻勾销,这种需要抉择 coroutineScope。相同,如果您心愿即便一个申请失败了其余的申请也要持续,则能够应用 supervisorScope,当一个协程失败了,supervisorScope 是不会勾销残余子协程的。

协程失败时收回报错信号

在协程中,报错信号是通过抛出异样来收回的,就像咱们平时写的函数一样。来自 suspend 函数的异样将通过 resume 从新抛给调用方来解决。跟惯例函数一样,您不仅能够应用 try/catch 这样的形式来处理错误,还能够构建形象来依照您喜爱的形式进行错误处理。

然而,在某些状况下,协程还是有可能会弄丢获取到的谬误的。

val unrelatedScope = MainScope()
// 失落谬误的例子
suspend fun lostError() {
   // 未应用结构化并发的 async
    unrelatedScope.async {throw InAsyncNoOneCanHearYou("except")
    }
}

留神: 上述代码申明了一个无关联协程作用域,它将不会依照结构化并发的形式启动新的协程。还记得我在一开始说的结构化并发是一系列编程语言个性和实际指南的汇合,在 suspend 函数中引入无关联协程作用域违反了结构化并发规定。

在这段代码中谬误将会失落,因为 async 假如您最终会调用 await 并且会从新抛出异样,然而您并没有去调用 await,所以异样就永远在那等着被调用,那么这个谬误就永远不会失去解决。

结构化并发保障当一个协程出错时,它的调用方或作用域会被告诉到。

如果您依照结构化并发的标准去编写上述代码,谬误就会被正确地抛给调用方解决。

suspend fun foundError() {
    coroutineScope {
        async {throw StructuredConcurrencyWill("throw")
        }
    }
}

coroutineScope 不仅会等到所有子工作都实现才会完结,当它们出错时它也会失去告诉。如果一个通过 coroutineScope 创立的协程抛出了异样,coroutineScope 会将其抛给调用方。因为咱们用的是 coroutineScope 而不是 supervisorScope,所以当抛出异样时,它会立即勾销所有的子工作。

应用结构化并发

在这篇文章中,我介绍了结构化并发,并展现了如何让咱们的代码配合 Android 中的 ViewModel 来避免出现工作透露。

同样,我还帮忙您更深刻去了解和应用 suspend 函数,通过确保它们在函数返回之前实现工作,或者是通过裸露异样来确保它们正确收回谬误信号。

如果咱们应用了不合乎结构化并发的代码,将会很容易呈现协程透露,即调用方不知如何追踪工作的状况。这种状况下,工作是无奈勾销的,同样也不能保障异样会被从新抛出来。这样会使得咱们的代码很难了解,并可能会导致一些难以追踪的 bug 呈现。

您能够通过引入一个新的不相干的 CoroutineScope (留神是大写的 C),或者是应用 GlobalScope 创立的全局作用域,然而这种形式的代码不合乎结构化并发要求的形式。

然而当呈现须要协程比调用方的生命周期更长的状况时,就可能须要思考非结构化并发的编码方式了,只是这种状况比拟常见。因而,应用结构化编程来追踪非结构化的协程,并进行错误处理和工作勾销,将是十分不错的做法。

如果您之前始终未依照结构化并发的办法编码,一开始的确一段时间去适应。这种构造的确保障与 suspend 函数交互更平安,应用起来更简略。在编码过程中,尽可能多地应用结构化并发,这样让代码更易于保护和了解。

在本文的开始列举了结构化并发为咱们解决的三个问题:

  1. 勾销工作 —— 当某项工作不再须要时勾销它;
  2. 追踪工作 —— 当工作正在执行时,追踪它;
  3. 收回谬误信号 —— 当协程失败时,收回谬误信号表明有谬误产生。

实现这种结构化并发,会为咱们的代码提供一些保障:

  1. 作用域勾销 时,它外部所有的 协程也会被勾销
  2. suspend 函数返回 时,意味着它的所有 工作都已实现
  3. 协程报错 时,它所在的 作用域或调用方会收到报错告诉

总结来说,结构化并发让咱们的代码更平安,更容易了解,还防止了呈现工作透露的状况。

下一步

本篇文章,咱们探讨了如何在 Android 的 ViewModel 中启动协程,以及如何在代码中使用结构化并发,来让咱们的代码更易于保护和了解。

在下一篇文章中,咱们将探讨如何在理论编码过程中应用协程,感兴趣的读者请持续关注咱们的更新。

退出移动版