接上一篇文章《在 Android 开发中使用协程 | 背景介绍》本文是Android协程介绍系列的第二篇。在任务开始执行后跟踪任务。跟踪协程在本系列的第一篇文章中,我们探讨了协程适合解决哪些问题。这里简单回顾一下,协程适合解决以下两种常见的编程问题:处理耗时任务(Longrunningtasks),这类任务经常会阻塞主线程;保证主线程安全(Main-safety),即保证从主线程调用任何挂起函数是安全的。协程通过在常规函数之上添加两个操作suspend和resume来解决上述问题。当特定线程上的所有协程都被挂起时,线程可以释放资源来处理其他任务。协程本身不会跟踪正在处理的任务,但是拥有成百上千个协程并同时对它们执行未决操作并没有错。协程是轻量级的,但是它们处理的任务不一定是轻量级的,比如读取文件或者发送网络请求。使用代码手动跟踪数千个协程非常困难。你可以尝试跟踪所有协程,手动确保它们全部完成或全部取消,那么代码会臃肿且容易出错。如果代码不完善,就会失去协程的踪迹,这就是所谓的“工作泄漏”情况。工作泄漏意味着协程丢失并且无法被跟踪。它类似于内存泄漏,但比它更糟糕,让丢失的协程可以自我恢复,从而占用内存、CPU、磁盘资源,甚至发起网络请求,这意味着它所占用的资源都不能被重用.泄漏协程会浪费内存、CPU、磁盘资源,甚至发送无用的网络请求。为了避免协程泄漏,Kotlin引入了结构化并发(structuredconcurrency)机制,它结合了一系列编程语言的特性和实用指南,遵循它可以帮助你跟踪运行在协程中的所有任务。在Android平台上,我们可以使用结构化并发来做以下三件事:取消任务——当一个任务不再需要时取消;跟踪任务——在任务执行时跟踪任务;信号错误-当协程失败时,会发出错误信号以指示发生错误。接下来,我们就以上几点逐一讨论,看看结构化并发是如何帮助跟踪所有协程而不会造成泄漏的。结构化并发:https://kotlinlang.org/docs/reference/coroutines/basics.html#structured-concurrency使用范围来取消任务在Kotlin中,定义协程必须指定其CoroutineScope。CoroutineScope即使在挂起时也可以跟踪协程。与第一篇文章中的Dispatcher不同,CoroutineScope不运行协程,它只是确保您不会忘记它们。为了确保跟踪所有协程,Kotlin不允许在不使用CoroutineScope的情况下启动新的协程。CoroutineScope可以被认为是具有超能力的ExecutorService的轻量级版本。它可以启动一个新的协程,这个协程也有我们在第一部分提到的suspend和resume的优点。CoroutineScope跟踪所有协程,同样它可以取消所有由它启动的协程。这在Android开发中非常有用,比如可以在用户离开界面时停止协程的执行。CoroutineScope跟踪所有协程,并可以取消所有由它启动的协程。需要注意的是,启动一个新的协程是你不能随便在某处调用suspend函数,suspend和resume机制需要你从常规函数切换到协程。启动协程有两种方式,适用于不同的场景:launchbuilder适用于“setandforget”工作,意味着它可以启动一个新的协程而不返回结果给调用者;async构建器可以启动一个新的协程,并允许您使用称为await的挂起函数返回结果。通常,您应该使用launch从常规函数中启动一个新协程。由于常规函数不能调用await(记住,它不能直接调用挂起函数),因此使用async作为协程的主要启动方法没有多大意义。稍后我们将讨论如何使用async。您应该改用协程作用域来调用启动方法来启动协程。scope.launch{//这段代码在作用域中启动一个新的协程//它可以调用挂起函数fetchDocs()}你可以把launch想象成一个桥梁,将代码从常规函数发送到协程世界。在launch函数体内,可以调用suspend函数,可以保证主线程的安全,正如我们在上一篇文章中介绍的那样。Launch是将代码从常规函数带到协程世界的桥梁。注意:launch和async之间的一个很大区别是它们处理异常的方式。async期望通过调用await最终得到结果(或异常),所以默认情况下它不会抛出异常。这意味着如果使用async启动一个新的协程,它将默默地抛出异常。由于启动和异步仅在CouroutineScope中可用,因此您创建的任何协程都将被该范围跟踪。Kotlin禁止您创建无法跟踪的协程,从而避免协程泄漏。启动https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html异步https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html在ViewModel中启动协程由于CoroutineScope会跟踪它启动的所有协程,而launch会创建一个新的协程,那么你在哪里调用launch并将其放入范围?在什么时候取消范围内启动的所有协程?在Android平台上,您可以将CoroutineScope实现与用户界面相关联。这使您可以避免内存泄漏或对不再与用户相关的Activity或Fragment执行额外的工作。当用户离开界面时,与界面关联的CoroutineScope可以取消所有不必要的任务。结构化并发可以保证当一个作用域被取消时,在它内部创建的所有协程也将被取消。在集成协程和安卓架构组件(AndroidArchitectureComponents)时,你会经常需要在ViewModel中启动协程。因为大部分任务都是在这里处理的,所以从这里开始是一种非常合理的方式,你不用担心旋转屏幕方向会终止你创建的协程。从AndroidXLifecycle2.1.0版本(2019年9月发布)开始,我们通过添加扩展属性ViewModel.viewModelScope在ViewModel中添加了对协程的支持。看下面的例子:classMyViewModel():ViewModel(){funuserNeedsDocs(){//当viewModelScope被清除时(调用onCleared()回调时)在ViewModel中启动一个新的协程viewModelScope.launch{fetchDocs()}}}),它会自动取消它启动的所有协程。这是标准做法,如果用户在获取数据之前关闭应用程序,让请求继续完成是浪费电量。为了增加安全性,CoroutineScope传播自身。也就是说,如果一个协程启动另一个新的协程,它们将在同一范围内终止。这意味着即使您依赖的代码库从您创建的viewModelScope启动协程,您也有办法取消它。注意:当协程挂起时,系统会通过抛出CancellationException的方式协同取消协程。捕获顶级异常(如Throwable)的异常处理程序将捕获此异常。如果你在做异常处理的时候消费了这个异常,或者从来没有执行过挂起操作,那么协程就会徘徊在半取消状态。因此,当您需要使协程与ViewModel的生命周期保持一致时,请使用viewModelScope从常规函数切换到协程。然后,viewModelScope会自动给你取消协程,所以这里就算写死循环,也完全不会泄露。下面的例子:funrunForever(){//在ViewModel中启动一个新的协程viewModelScope.launch{//当ViewModel清空后,下面的代码也会被取消while(true){delay(1_000)//每1秒做点什么}}}通过使用viewModelScope,您可以确保所有任务(包括无限循环)都可以在不需要时取消。协同取消:https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html#cancellation-and-timeoutsTaskTracking使用协程来处理任务对于很多代码来说真的很方便。启动协程,发起网络请求,将结果写入数据库,一切自然流畅。但有时,你可能会遇到稍微复杂一点的问题,比如你需要在一个协程中同时处理两个网络请求,这时就需要启动更多的协程。要创建多个协程,可以在suspend函数中使用名为coroutineScope或supervisorScope的构造函数来启动多个协程。但老实说,API有点令人困惑。coroutineScope构造函数和CoroutineScope只是一个字母的区别,却是完全不同的东西。另外,如果任意启动新的协程,可能会导致潜在的任务泄漏(workleak)。调用者可能不知道启用了新协程,这意味着无法跟踪它。为了解决这个问题,结构化并发发挥了作用,它保证当suspend函数返回时,意味着它处理的所有任务也已经完成。结构化并发保证当挂起函数返回时,它处理的所有任务都已完成。该示例使用coroutineScope来获取两个文档内容:首先,协程通过启动以“一劳永逸”的方式启动,这意味着它不会向调用者返回任何结果。二是通过async获取文档,所以会有返回值。不过上面的例子有点奇怪,因为一般来说,两个文件都应该使用async来获取,但是这里我只是想举个例子来说明,大家可以根据自己的情况选择使用launch或者async,或者两者混合使用需要。coroutineScope和supervisorScope允许您从挂起函数安全地启动协程。但是请注意,这段代码并没有明确地等待两个创建的协程在返回之前完成它们的任务。当fetchTwoDocs返回时,协程仍在运行。因此,为了实现结构化并发并避免泄漏,我们要确保当fetchTwoDocs等挂起函数返回时,它们正在执行的所有任务也将终止。换句话说,在fetchTwoDocs返回之前,它启动的所有协程也将能够完成它们的任务。Kotlin使用coroutineScope构造函数确保fetchTwoDocs不会泄漏,coroutinScope将自身挂起并等待在其内部启动的所有协程完成后再返回。因此,只有在coroutineScope构建器中启动的所有协程都已完成其任务后,fetchTwoDocs函数才会返回。coroutineScope:https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.htmlsupervisorScope:https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/supervisor-scope.html处理一堆任务现在我们已经成功地跟踪了一两个协程,让我们有一个激动人心的时刻,尝试跟踪一千个协程!看看下面的动画这个动画展示了coroutineScope如何跟踪一千个协程这个动画展示了我们如何同时发出一千个网络请求。当然,在真正的Android开发中最好不要这样做,太浪费资源了。在此代码中,我们在coroutineScope构造函数中使用launch启动了一千个协程,您可以看到它们是如何联系在一起的。由于我们使用的是suspend函数,因此代码必须使用CoroutineScope创建协程。我们目前对这个CoroutineScope一无所知,它可能是一个viewModelScope或在别处定义的CoroutineScope,但无论哪种方式,coroutineScope构造函数都会将它用作它创建的新作用域的父级。然后,在coroutineScope代码块中,launch将在新范围内启动协程,范围将在协程启动和完成时跟踪它。最后,一旦在coroutineScope中启动的所有协程都已完成,loadLots方法就可以轻松返回。注意:作用域和协程之间的父子关系是使用Job对象创建的。不过不用深究,知道就好。coroutineScope和supervisorScope将等待所有子协程完成。上面的要点是coroutineScope和supervisorScope可用于从任何挂起函数安全地启动协程。即使你启动一个新的协程,也不会有泄漏,因为调用者总是被挂起,直到新的协程完成。更重要的是,coroutineScope会创建一个子作用域,所以一旦父作用域被取消,它就会将取消消息传递给所有新的协程。如果调用者是viewModelScope,这千个协程会在用户离开界面后自动取消,非常简洁高效。在继续讨论报错相关问题之前,有必要花点时间讨论一下supervisorScope和coroutineScope。它们之间的主要区别在于,当任何子作用域失败时,coroutineScope将被取消。如果网络请求失败,所有其他请求将立即取消。对于此需求,请选择coroutineScope。相反,如果你希望即使一个请求失败,其他请求也能继续,你可以使用supervisorScope。当一个协程失败时,supervisorScope不会取消剩余的子协程。协程失败时发送错误信号在协程中,通过抛出异常来发送错误信号,就像我们平时写的函数一样。suspend函数的异常会通过resume重新抛给调用者处理。与常规函数一样,您不仅可以使用try/catch之类的东西来处理错误,还可以构建抽象以按照您喜欢的方式处理错误。但是,在某些情况下,协程可能仍然会丢失它得到的错误。valunrelatedScope=MainScope()//丢失错误示例suspendfunlostError(){//不使用结构化并发asyncunrelatedScope.async{throwInAsyncNoOneCanHearYou("except")}}注意:以上代码声明了一个不相关的协程作用域,该作用域不会启动新的协程以结构化的并发方式。记住我一开始所说的,结构化并发是编程语言特性和实用指南的集合。suspend函数中引入非关联协程作用域违反了结构化并发的规则。在此代码中,错误将丢失,因为异步假定您最终将调用await并且异常将被重新抛出,但是您没有调用await,因此异常将始终在那里等待被调用,那么错误将永远不会出现处理。结构化并发保证当协程失败时,它的调用者或作用域会得到通知。如果按照结构化并发的规范编写上面的代码,错误会被正确的抛给调用者处理。suspendfunfoundError(){coroutineScope{async{throwStructuredConcurrencyWill("throw")}}}coroutineScope不仅会等待所有子任务完成后再结束,还会在它们出错时收到通知。如果coroutineScope创建的协程抛出异常,coroutineScope会抛给调用者。因为我们使用的是coroutineScope而不是supervisorScope,所以它会在抛出异常时立即取消所有子任务。使用结构化并发在这篇文章中,我介绍了结构化并发并展示了如何使我们的代码与Android中的ViewModel一起工作以避免任务泄漏。同样,我还帮助您更深入地理解和使用挂起函数,方法是确保它们在函数返回之前完成任务,或者通过公开异常以确保它们正确地发出错误信号。如果我们使用不符合结构化并发的代码,很容易出现协程泄漏,即调用者不知道如何跟踪任务。在这种情况下,无法取消任务,并且无法保证会重新抛出异常。这将使我们的代码难以理解,并可能导致一些难以跟踪的错误。可以通过引入一个新的不相关的CoroutineScope(注意大写的C)来创建一个全局作用域,或者使用GlobalScope,但是这种方式的代码不符合结构化并发的要求。但是当出现协程的生命周期比调用者的生命周期长的情况时,可能需要考虑非结构化并发的编码方式,但这种情况比较少见。因此,使用结构化编程来跟踪非结构化协程,并执行错误处理和任务取消将是一个很好的做法。如果你之前没有按照结构化并发的方式编码过,一开始需要一段时间适应。这种结构确实保证了与挂起函数的交互更安全,使用起来更简单。在编码时,尽可能使用结构化并发,使代码更易于维护和理解。在本文的开头,我列出了结构化并发为我们解决的三个问题:取消任务——当一个任务不再需要时取消;跟踪任务-在执行任务时跟踪任务;发出错误信号——当协程失败时,会发出错误信号以表明出现了问题。实现这种结构化的并发,会为我们的代码提供一些保障:当作用域被取消时,它里面的所有协程也会被取消;当suspend函数返回时,意味着它的所有任务都已经完成;协程报告错误,其作用域或调用者将收到错误通知。综上所述,结构化并发让我们的代码更安全,更容易理解,避免任务泄露。在本文接下来,我们讨论了如何在Android的ViewModel中启动协程,以及如何在代码中使用结构化并发来让我们的代码更易于维护和理解。在下一篇文章中,我们将探讨如何在实际编码过程中使用协程,感兴趣的读者请继续关注我们的更新。【本文为栏目组织《GoogleDevelopers》原创稿件,转载请联系原作者(微信公众号:Google_Developers)】点此查看更多本作者好文
