Android Kotlin协程

裴鸿熙
2023-12-01

Recently, Kotlin Coroutines introduce an advanced and efficient approach of concurrency design pattern, which can be used on Android to simplify asynchronous codes. As a matter of fact, this approach is much more simple, comprehensive, and robust in comparison with other approaches in Android. This essay will introduce and discuss some key concepts of Kotlin Coroutines for Android developers.

最近,Kotlin Coroutines引入了一种先进且高效的并发设计模式方法,该方法可在Android上用于简化异步代码。 实际上,与Android中的其他方法相比,该方法更加简单,全面和可靠。 本文将为Android开发人员介绍和讨论Kotlin Coroutines的一些关键概念。

简介与概述 (Introduction and Overview)

Basically, a Coroutine is a concurrency design pattern, which you can use on Android to simplify codes that execute asynchronously. In fact, Coroutines were added to Kotlin in version 1.3, and are based on established some concepts from other programming languages.

基本上, 协程是一种并发设计模式,您可以在Android上使用它来简化异步执行的代码。 实际上, 协程是在1.3版中添加到Kotlin的,并且基于其他编程语言中已建立的一些概念。

The question is: what problems in Android are Coroutines trying to solve?

问题是: 协程试图解决Android中的哪些问题?

Fundamentally, whenever we consider asynchronous programming, we cannot neglect synchronous programming wholly due to simplicity in implementation. In this example, we have the loadData function, which shows the result of a networkRequest.

从根本上讲,每当考虑异步编程时,由于实现简单,我们不能完全忽略同步编程。 在此示例中,我们具有loadData函数,该函数显示networkRequest的结果。

fun loadData() {   val data = networkRequest()   show(data)}

Because this is synchronous, it will block the thread where this has been running on. Thus, imagine if we call loadData from the main UI thread, networkRequest will block its thread whenever it is waiting for that networkRequest to occur. This means, if the Operating System want to call onDraw as another function; therefore, the user will observe a frozen UI and unresponsive app indeed. Also, it will get unblocked when the networkRequest ends. So, how can we move this networkRequest to an another thread? How can we make this code asynchronous? A solution for this issue is using callbacks.

因为这是同步的,所以它将阻塞正在其上运行的线程。 因此,想象一下,如果我们从主UI线程调用loadData,则当NetworkRequest等待该networkRequest发生时,它将阻塞其线程。 这意味着,如果操作系统要调用onDraw作为另一个函数; 因此,用户确实会观察到冻结的UI和无响应的应用程序。 另外,当networkRequest结束时,它将不受阻碍。 那么,如何将这个networkRequest移到另一个线程? 我们如何使该代码异步? 解决此问题的方法是使用回调。

fun loadData() {   networkRequest { data  ->     show(data)   }
}

As we can see in this example, if we execute above code, we notice that networkRequest will be called, and we will observe later that the function networkRequest will move the execution of the networkRequest to a different thread. In other words, the main UI thread would be free, and the Operating System could call onDraw. This means refreshing the screen will be done as much as possible. Whenever the networkRequest ends up, it will call this lambda, a callback, which shows the result of the networkRequest on the screen. networkRequest is just a function that does not return data anymore, and return Unit. Instead, it takes in a callback, a lambda that we call in this case onSuccess, with what to perform after finishing the networkRequest. So, we can be able to call DefaultScheduler.execute to move the execution a different thread, and after finishing the task, we can post to the main thread by calling onSuccess.

在本示例中可以看到,如果执行上述代码,则会注意到将调用networkRequest,稍后将观察到函数networkRequest会将networkRequest的执行移至另一个线程。 换句话说,主UI线程将是免费的,并且操作系统可以调用onDraw。 这意味着将尽可能多地刷新屏幕。 每当networkRequest结束时,它将调用此lambda,这是一个回调,它在屏幕上显示networkRequest的结果。 networkRequest只是一个不再返回数据并返回Unit的函数。 取而代之的是,它接收一个回调,即在本例中为onSuccess调用的lambda,其中包含完成networkRequest之后要执行的操作。 因此,我们可以调用DefaultScheduler.execute将执行移动到另一个线程,完成任务后,可以通过调用onSuccess将其发布到主线程。

fun networkRequest(onSuccess : (Data) -> Unit) {  DefaultScheduler.execute {     postToMainThread(onSuccess(result))  }
}

Even though callbacks could be an acceptable solution for some use cases, it has some problems to cope with. This example is a simple function that it just only creates another request, and displays something on the screen. However, the logic can be more complicated. For instance, if we want to add some extra nesting networkRequest functions after the first one happens. This leads to Callback Hell, which makes codes hard to read and debug.

尽管对于某些用例,回调可能是可接受的解决方案,但它仍有一些问题需要解决。 这个例子是一个简单的函数,它仅创建另一个请求,并在屏幕上显示内容。 但是,逻辑可能更复杂。 例如,如果我们想在第一个函数发生后添加一些额外的嵌套networkRequest函数。 这会导致回调地狱,使代码难以阅读和调试。

The simplicity of synchronous code with all the power of asynchronicity can be an idealistic approach. This is where you can use Coroutines as a solution. For instance, we have the same loadData function that is written with Coroutines. The only difference that we use suspend modifier in the function definition. In short, this modifier notifies the Kotlin compiler that this function requires to be executed within a Coroutine.

具有所有异步功能的同步代码的简单性可以是理想的方法。 您可以在这里使用协程作为解决方案。 例如,我们有与Coroutines编写的相同的loadData函数。 我们在函数定义中使用suspend修饰符的唯一区别。 简而言之,此修饰符通知Kotlin编译器该功能需要在协程中执行。

Suspend fun loadData() {  val data = networkRequest()  show(data)
}

Important Note: Do not mark function suspend unless you are forced to.

重要说明:除非强制执行,否则请勿将功能标记为暂停。

Initially, suspend functions might suspend the execution of the current Coroutine without blocking the current thread. Also, instead of returning a value, it knows in which context the caller suspended it. As a result, by using this feature, it can resume appropriately when ready. This helps further in memory optimizations of the CPU and multi-tasking. In spite of blocking a thread, suspending functions are less expensive because they do not require a context switching. A suspending function can be created by adding the keyword suspend to a function.

最初,暂停函数可能会在不阻塞当前线程的情况下暂停当前协程的执行。 另外,它不返回值,而是知道调用者在哪个上下文中挂起了它。 结果,通过使用此功能,它可以在准备就绪时适当地恢复。 这有助于进一步优化CPU和多任务的内存。 尽管阻塞了线程,但由于挂起函数不需要上下文切换,因此挂起函数的成本较低。 可以通过在函数中添加关键字“ suspend”来创建函数。

All in all, Coroutines help solve two significant issues on Android:

总而言之,协程可帮助解决Android上的两个重要问题:

  1. Handle long-running tasks that might block the main thread and cause your app to freeze and unresponsive.

    处理长时间运行的任务,这些任务可能会阻塞主线程并导致您的应用程序冻结和无响应。

2. Supporting main-safety in calling network or disk operations from the main thread.

2.支持从主线程调用网络或磁盘操作的主安全性

连续通过式编程(CPS) (Continuation-Passing Style programming(CPS))

As a matter of fact, Kotlin Coroutines use the concept of Continuation-Passing Style programming(CPS). This method of programming includes passing the control flow of the program as an argument to functions. This argument is called Continuation in Kotlin. In a word, a continuation is similar to a callback, but it is more system level generally. Furthermore, Coroutines can be suspended and resumed. It means you can have a long-running task that you can execute gradually. So, you can be able to pause it any number of times, and resume it when you want to use it again. An important note is that using a number of Kotlin Coroutines will not produce memory overheads to your app.

实际上,Kotlin协程使用了连续传递样式编程(CPS)的概念。 这种编程方法包括将程序的控制流作为参数传递给函数。 这个论点在Kotlin被称为延续。 简而言之,延续类似于回调,但一般来说是系统级的。 此外,协程可以暂停和恢复。 这意味着您可以执行可以长期执行的任务。 因此,您可以暂停它多次,然后在您想再次使用它时将其恢复。 一个重要的注意事项是,使用许多Kotlin协程不会对您的应用程序产生内存开销。

Coroutines = Co + Routines

协程= Co +例程

Co means cooperation and Routines means functions. This means that when functions cooperate with each other, we call it as Coroutines.

Co表示合作, Routines表示功能 这个 意味着当函数彼此协作时,我们将其称为协程。

执行不同的线程 (Executing a different thread)

The question is: how can we execute networkRequest to a different thread? You must use withContext(). In fact, withContext() is a suspend function from the Coroutines library that takes a dispatcher as a parameter.

问题是:我们如何对另一个线程执行networkRequest? 您必须使用withContext() 。 实际上, withContext()是Coroutines库中的暂停函数,该函数将调度程序作为参数。

suspend fun networkRequest(): Data=      withContext(Dispatchers.IO) {        //...  
}

Dispatchers is basically a way to mention: I want to run this computation in this particular thread. Inside withContext(), we can have our blocking networkRequest code, and it does not matter if it is blocking the IO thread. In a word, withContext() does not add extra overhead compared to an equivalent callback-based implementation.

分派器基本上是一种提及方式:我想在此特定线程中运行此计算。 在withContext()内部,我们可以使用阻塞的networkRequest代码,并且是否阻塞IO线程也没有关系。 简而言之,与基于回调的等效实现相比,withContext()不会增加额外的开销。

Kotlin Coroutines use dispatchers to determine which threads are used for coroutine execution. To run code outside of the main thread, you can tell Kotlin Coroutines to perform work on either the Default or IO dispatcher.

Kotlin协程使用调度程序来确定将哪些线程用于协程执行。 要在主线程之外运行代码,您可以告诉Kotlin Coroutines在Default或IO调度程序上执行工作。

To be exact, Kotlin supports three main dispatchers that you can use in your Android apps:

确切地说,Kotlin支持三种主要的调度程序,您可以在Android应用程序中使用它们:

  • Dispatchers.Main: Use this dispatcher to run a coroutine on the main thread. This should be used only for interacting with the UI and performing quick work. For example, calling suspend functions, running Android UI framework operations, and updating LiveData objects.

    Dispatchers.Main:使用此调度程序在主线程上运行协程。 这仅应用于与UI交互并执行快速工作。 例如,调用暂LiveData函数,运行Android UI框架操作以及更新LiveData对象。

  • Dispatchers.IO: This dispatcher is optimized to perform disk or network I/O outside of the main thread. For instance, using the Room, reading from or writing to files, and running any network operations.

    Dispatchers.IO:此调度程序经过优化,可以在主线程之外执行磁盘或网络I / O。 例如,使用Room ,读取或写入文件以及运行任何网络操作。

  • Dispatchers.Default: This dispatcher is optimized to perform CPU-intensive work outside of the main thread like sorting a list and parsing JSON.

    Dispatchers.Default:此调度程序经过优化,可以在主线程之外执行CPU密集型工作,例如对列表进行排序和解析JSON。

Kotlin协程的作用域 (Scopes in Kotlin Coroutines)

As a matter of fact, Scopes in Kotlin Coroutines are extremely helpful because we have to cancel the background task as soon as the activity is destroyed. Kotlin Coroutines must run in an element, which is called a CoroutinesScope. A CoroutinesScope keeps track of your Coroutines, even Coroutines that are suspended. In short, CoroutinesScope just only makes sure that you keep track them, and it cancel all all of the Coroutines started in it.

实际上,Kotlin Coroutines中的Scopes非常有用,因为一旦活动被销毁,我们必须立即取消后台任务。 Kotlin协程必须在一个元素中运行,该元素称为CoroutinesScope 。 CoroutinesScope会跟踪您的协程,甚至是已暂停的协程。 简而言之,CoroutinesScope仅确保您对其进行跟踪,并取消其中启动的所有Coroutines。

In addition, you can connect CoroutineScope implementations with a component lifecycle. This allows you avoid memory leaks or some extra works for activities or fragments, which are no longer relevant to the user. By using Jetpack components, they fit naturally in a ViewModel because ViewModel is not destroyed during configuration changes like screen rotation. So, you do not have to worry about your Coroutines getting canceled or restarted. You can cancel everything that was started in the scope at any time. Besides, Scopes propagate themselves. This means if a coroutine starts another coroutine, both Coroutines have the same scope.

此外,您可以将CoroutineScope实现与组件生命周期相连接。 这样可以避免内存泄漏或活动或片段的多余工作,而这些活动或片段不再与用户相关。 通过使用Jetpack组件,它们自然适合安装在ViewModel中,因为在配置更改(如屏幕旋转)期间不会破坏ViewModel。 因此,您不必担心协程被取消或重新启动。 您可以随时取消在范围内启动的所有内容。 此外,范围会自行传播。 这意味着,如果一个协程开始另一个协程,则两个协程具有相同的作用域。

A Scope is just a simple variable that is really cheap to create. It does not hold references to heavy objects. Therefore, whenever you want to control the lifecycle of a Coroutine, you can create a CoroutineScope. In this example, we can use this Scope to trigger the computation:

范围只是一个简单的变量,创建起来确实很便宜。 它不包含对重物的引用。 因此,每当您要控制协程的生命周期时,都可以创建CoroutineScope。 在此示例中,我们可以使用此Scope触发计算:

val scope = CoroutineScope(Dispatchers.Main)fun onButtonClicked() {   scope.launch {      loadData()
}
}

Additionally, whenever we do not need the scope anymore, for example, if we are in a ViewModel, we can call onCleared() as well as scope.cancel(). Canceling a scope means that it will cancel all the children Coroutines that it started.

另外,每当不再需要范围时,例如,如果我们在ViewModel中,则可以调用onCleared()以及scope.cancel()取消作用域意味着它将取消其启动的所有子协程。

创建协程 (Create Coroutines)

Initially, you can crate Coroutines in two ways:

最初,您可以通过两种方式来创建协程:

  • launch starts a new coroutine and does not return the result to the caller. Also, it is considered "fire and forget". For example, we can upload logs that happened in our application as follows:

    launch启动新的协程,并且不会将结果返回给调用方。 同样,它被认为是“火与遗”。 例如,我们可以上传应用程序中发生的日志,如下所示:

scope.launch(Dispatchers.IO) {   loggingService.upload(logs)}
  • async starts a new coroutine and allows you to return a result with a suspend function called await. For instance, getUser is a function and takes a user ID as a parameter. So, it returns a user object. We can assume a deferred object as a Future or a Promise in Java. So, with that future, or that deferred object, you can call await and it will wait until async has finished its computation, and it will return the value of that Coroutine.

    async启动一个新的协程,并允许您使用名为await的暂停函数返回结果。 例如,getUser是一个函数,并以用户ID作为参数。 因此,它返回一个用户对象。 我们可以将延迟对象假定为Java中的Future或Promise。 因此,对于那个将来的对象或那个延迟的对象,您可以调用await,它将等待直到异步完成其计算,然后它将返回该协程的值。

suspend fun getUser(userId: String): User {   coroutineScope {     val deferred = async(Dispatchers.IO) {       userService.getUser(userId)     }     deferred.await()
}

Similarities between these two ways:

这两种方式之间的相似之处:

  1. Both of them take a dispatcher.

    他们俩都派遣调度员。

2. Both of them are executed in a scope.

2.两者均在范围内执行。

3. Launch and async are considered as an Extension Function on the scope.

3.启动和异步被视为示波器的扩展功能。

4. They are not suspend functions. Therefore, launch and async are the entry points to Coroutines.

4.它们不是挂起功能。 因此,启动和异步是协同程序的入口点。

Another difference that can be indicated for these two ways is launch is throw the exception as soon as it happens, and async holds on that exception until you call await.

这两种方式的另一个不同之处是启动是在异常发生时立即引发异常,并且异步会保留该异常,直到您调用await为止。

取消昂贵的任务 (Canceling expensive tasks)

Whenever you want to accomplish an expensive task like reading files, we usually spend a lot of time reading the files, you have to cooperate and make this canceler work. The Coroutine or thread would be busy in reading files, and it could not be listening for cancellation. So, in this case you have to cooperate and check if the Coroutine is active or not. You can do that by checking or calling yield() or ensureActive() as follows:

每当您想要完成诸如读取文件之类的昂贵任务时,我们通常会花费大量时间读取文件,您必须配合并使此取消器起作用。 协程或线程将忙于读取文件,并且无法监听取消。 因此,在这种情况下,您必须配合检查协程是否处于活动状态。 您可以通过如下方式检查或调用yield()sureActive()来做到这一点:

scope.launch(Dispatchers.IO) {    for (name in files) {    yield()  //or using ensureActive()    readFile(name)    }
}

Whenever the Coroutine is cancelled and the function is called, then it will stop the execution of this Coroutine. As a result, if you want to do a heavy computation task, make sure that you will check for cancellation.

每当协程被取消并调用该函数时,它就会停止执行此协程。 因此,如果您要执行繁重的计算任务,请确保您将检查取消。

结论 (In conclusion)

This article introduced and considered some major concepts and issues of Android Kotlin Coroutines. As a matter of fact, this new approach can address two significant issues on Android effectively: handle long-running tasks asynchronously and supporting main-safety.

本文介绍并考虑了Android Kotlin Coroutines的一些主要概念和问题。 实际上,这种新方法可以有效解决Android上的两个重要问题:异步处理长时间运行的任务以及支持主安全。

翻译自: https://medium.com/kayvan-kaseb/android-kotlin-coroutines-9c1843f81d1d

 类似资料: