【译】使用kotlin协程提升app性能

原文android

协程是一种并发设计模式,您能够在Android上使用它来简化异步执行的代码。Kotlin1.3版本添加了 Coroutines,并基于其余语言的既定概念。数据库

Android上,协程有助于解决两个主要问题:设计模式

  • 管理长时间运行的任务,不然可能会阻止主线程并致使应用冻结。
  • 提供主安全性,或从主线程安全地调用网络或磁盘操做。

本主题描述了如何使用Kotlin协程解决这些问题,使您可以编写更清晰,更简洁的应用程序代码。安全

管理长时间运行的任务

Android上,每一个应用程序都有一个主线程来处理用户界面并管理用户交互。若是您的应用程序为主线程分配了太多工做,那么应用程序可能会明显卡顿或运行缓慢。网络请求,JSON解析,从数据库读取或写入,甚至只是迭代大型列表均可能致使应用程序运行缓慢,致使可见的缓慢或冻结的UI对触摸事件响应缓慢。这些长时间运行的操做应该在主线程以外运行。网络

如下示例显示了假设的长期运行任务的简单协程实现:架构

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)和返回以外,协同程序还添加了suspendresume并发

  • suspend暂停当前协同程序的执行,保存全部局部变量。
  • resume恢复从暂停的协同处继续执行暂停的协同程序。

您只能从其余suspend函数调用suspend函数,或者使用诸如启动之类的协程构建器来启动新的协程。框架

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

Kotlin使用堆栈框架来管理与任何局部变量一块儿运行的函数。挂起协程时,将复制并保存当前堆栈帧以供之后使用。恢复时,堆栈帧将从保存位置复制回来,而且该函数将再次开始运行。即便代码看起来像普通的顺序阻塞请求,协程也能够确保网络请求避免阻塞主线程。async

Use coroutines for main-safety

Kotlin协程使用调度程序来肯定哪些线程用于协程执行。要在主线程以外运行代码,您能够告诉Kotlin协程在Default或IO调度程序上执行工做。在Kotlin中,全部协同程序必须在调度程序中运行,即便它们在主线程上运行。协同程序能够暂停,调度程序负责恢复它们。

要指定协程应该运行的位置,Kotlin提供了三个可使用的调度程序:

  • Dispatchers.Main - 使用此调度程序在主Android线程上运行协同程序。 这应该仅用于与UI交互并执行快速工做。 示例包括调用挂起函数,运行Android UI框架操做以及更新LiveData对象。
  • Dispatchers.IO - 此调度程序已通过优化,可在主线程外执行磁盘或网络I / O. 示例包括使用Room组件,读取或写入文件以及运行任何网络操做。
  • Dispatchers.Default - 此调度程序已通过优化,能够在主线程以外执行CPU密集型工做。 示例用例包括对列表进行排序和解析JSON。

继续前面的示例,您可使用调度程序从新定义get函数。 在get的主体内部,调用withContext(Dispatchers.IO)来建立一个在IO线程池上运行的块。 放在该块中的任何代码老是经过IO调度程序执行。 因为withContext自己是一个挂起函数,所以函数get也是一个挂起函数。

使用协同程序,您能够调度具备细粒度控制的线程。 由于withContext()容许您控制任何代码行的线程池而不引入回调,因此您能够将它应用于很是小的函数,例如从数据库读取或执行网络请求。 一个好的作法是使用withContext()来确保每一个函数都是主安全的,这意味着您能够从主线程调用该函数。 这样,调用者永远不须要考虑应该使用哪一个线程来执行该函数。

在前面的示例中,fetchDocs()在主线程上执行; 可是,它能够安全地调用get,后者在后台执行网络请求。 由于协同程序支持挂起和恢复,因此只要withContext块完成,主线程上的协程就会以get结果恢复。

重要说明:使用suspend并不能告诉Kotlin在后台线程上运行函数。 暂停函数在主线程上运行是正常的。 在主线程上启动协同程序也很常见。 当您须要主安全时,例如在读取或写入磁盘,执行网络操做或运行CPU密集型操做时,应始终在挂起函数内使用withContext()

与等效的基于回调的实现相比,withContext()不会增长额外的开销。 此外,在某些状况下,能够优化withContext()调用,而不是基于等效的基于回调的实现。 例如,若是一个函数对网络进行十次调用,则能够经过使用外部withContext()告诉Kotlin只切换一次线程。 而后,即便网络库屡次使用withContext(),它仍然停留在同一个调度程序上,并避免切换线程。 此外,Kotlin优化了Dispatchers.Default和Dispatchers.IO之间的切换,以尽量避免线程切换。

要点:使用使用Dispatchers.IO或Dispatchers.Default等线程池的调度程序并不能保证该块从上到下在同一个线程上执行。 在某些状况下,Kotlin协程可能会在暂停和恢复后将执行移动到另外一个线程。 这意味着线程局部变量可能不会指向整个withContext()块的相同值。

指定CoroutineScope

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

CoroutineScope的一个重要功能是当用户离开应用程序中的内容区域时中止协程执行。 使用CoroutineScope,您能够确保正确中止任何正在运行的操做。

将CoroutineScope与Android架构组件配合使用

在Android上,您能够将CoroutineScope实现与组件生命周期相关联。这样能够避免泄漏内存或为与用户再也不相关的activityfragment执行额外的工做。使用Jetpack组件,它们天然适合ViewModel。因为ViewModel在配置更改(例如屏幕旋转)期间不会被销毁,所以您没必要担忧协同程序被取消或从新启动。

范围知道他们开始的每一个协同程序。这意味着您能够随时取消在做用域中启动的全部内容。范围传播本身,因此若是一个协程开始另外一个协同程序,两个协同程序具备相同的范围。这意味着即便其余库从您的范围启动协程,您也能够随时取消它们。若是您在ViewModel中运行协同程序,这一点尤其重要。若是由于用户离开了屏幕而致使ViewModel被销毁,则必须中止它正在执行的全部异步工做。不然,您将浪费资源并可能泄漏内存。若是您在销毁ViewModel后应该继续进行异步工做,则应该在应用程序架构的较低层中完成。

警告:经过抛出CancellationException协同取消协同程序。 在协程取消期间触发捕获异常或Throwable的异常处理程序。

使用适用于Android体系结构的KTX库组件,您还可使用扩展属性viewModelScope来建立能够运行的协同程序,直到ViewModel被销毁。

启动一个协程

您能够经过如下两种方式之一启动协同程序:

  • launch会启动一个新的协程,而且不会将结果返回给调用者。 任何被认为是“发射并忘记”的工做均可以使用launch来开始。
  • async启动一个新的协同程序,并容许您使用名为await的挂起函数返回结果。

一般,您应该从常规函数启动新协程,由于常规函数没法调用等待。 仅在另外一个协同程序内部或在挂起函数内部执行并行分解时才使用异步。

在前面的示例的基础上,这里是一个带有viewModelScope KTX扩展属性的协程,它使用launch从常规函数切换到协同程序:

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

警告:启动和异步处理异常的方式不一样。 因为async指望在某个时刻最终调用await,它会保留异常并在await调用中从新抛出它们。 这意味着若是您使用await从常规函数启动新的协同程序,则可能会以静默方式删除异常。 这些丢弃的异常不会出如今崩溃指标中,也不会出如今logcat中。

并行分解

当函数返回时,必须中止由挂起函数启动的全部协同程序,所以您可能须要保证这些协程在返回以前完成。 经过Kotlin中的结构化并发,您能够定义一个启动一个或多个协同程序的coroutineScope。 而后,使用await()(对于单个协同程序)或awaitAll()(对于多个协程),能够保证这些协程在从函数返回以前完成。

例如,让咱们定义一个以异步方式获取两个文档的coroutineScope。 经过在每一个延迟引用上调用await(),咱们保证在返回值以前两个异步操做都完成:

suspend fun fetchTwoDocs() =
  coroutineScope {
    val deferredOne = async { fetchDoc(1) }
    val deferredTwo = async { fetchDoc(2) }
    deferredOne.await()
    deferredTwo.await()
  }
复制代码

即便fetchTwoDocs()使用异步启动新的协同程序,该函数也会使用awaitAll()等待那些启动的协同程序在返回以前完成。 但请注意,即便咱们没有调用awaitAll(),coroutineScope构建器也不会恢复调用fetchTwoDocs的协程,直到全部新的协程完成。

此外,coroutineScope捕获协程抛出的任何异常并将它们路由回调用者。

有关并行分解的更多信息,请参阅编写挂起函数。

具备内置支持的架构组件

一些体系结构组件(包括ViewModel和Lifecycle)经过其本身的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
}
复制代码
相关文章
相关标签/搜索