Ali P7 masters teach you Kotlin coroutines (1) - Getting Started

Cracking Kotlin Coroutines (1) - Getting Started

insert image description here

I've written about coroutines before, a long time ago. It was still very painful at that time. After all, such a powerful framework as kotlinx.coroutines was still in its infancy, so the few coroutine articles I wrote were almost telling everyone how to write such a framework-that feeling was terrible. Yes, because few people will have such a demand.

This time I am going to write it from the perspective of coroutine users (that is, programmers, you and me), and I hope it will be helpful to everyone.

2. Demand Confirmation

Before we start explaining coroutines, we need to confirm a few things:

  1. You used threads, right?
  2. You wrote callbacks, right?
  3. Have you used frameworks like RxJava?

Check out your answer:

  • If the answer to the above questions is "Yes", then great, this article is for you, because you have realized how scary callbacks are and found a solution;
  • If the first two are "Yes", no problem, at least you have started using callbacks, and you are a potential user of coroutines;
  • If only the first one is "Yes", then maybe you are just starting to learn threads, then you should lay the foundation first and then come again~

3. A general example

We send a network request through Retrofit, where the interface is as follows:

interface GitHubServiceApi {
    @GET("users/{login}")
    fun getUser(@Path("login") login: String): Call<User>
}

data class User(val id: String, val name: String, val url: String)

Retrofit is initialized as follows:

val gitHubServiceApi by lazy {
    val retrofit = retrofit2.Retrofit.Builder()
            .baseUrl("https://api.github.com")
            .addConverterFactory(GsonConverterFactory.create())
            .build()

    retrofit.create(GitHubServiceApi::class.java)
}

Then when we request the network:

gitHubServiceApi.getUser("bennyhuo").enqueue(object : Callback<User> {
    override fun onFailure(call: Call<User>, t: Throwable) {
        handler.post { showError(t) }
    }

    override fun onResponse(call: Call<User>, response: Response<User>) {
        handler.post { response.body()?.let(::showUser) ?: showError(NullPointerException()) }
    }
})

After the request result comes back, we switch the thread to the UI thread to display the result. This kind of code exists in a large number in our logic, what's wrong with it?

  • With Lambda expressions, we make thread switching less obvious, but it still exists, and once the developer misses it, there will be problems here
  • The callback is nested two layers, which seems to be nothing, but the logic in the real development environment must be more complicated than this, such as retrying failed login
  • Repeated or scattered exception handling logic, we call it once when the request fails showError, and call it again when the data read fails, there may be more repetitions in the real development environment

Kotlin's own syntax already makes this code look much better. If you write it in Java, your intuition will tell you: you are writing a bug.

If you are not an Android developer, then you may not know what a handler is, it doesn't matter, you can replace it with SwingUtilities.invokeLater{ ... }(Java Swing), or setTimeout({ ... }, 0)(Js) and so on.

4. Transform into a coroutine

Of course you can transform into RxJava style, but RxJava is more abstract than coroutines, because unless you are proficient in using those operators, you don't know what it is doing at all (just imagine) retryWhen. Coroutines are different. After all, with the support of the compiler, it can express the logic of the code very concisely. Don’t think about the implementation logic behind it. Its running result is what your intuition tells you.

For Retrofit, there are two ways to transform it into a coroutine, namely through the CallAdapter and suspend functions.

4.1 The way of CallAdapter

Let's take a look at the CallAdapter method first. The essence of this method is to let the interface method return a coroutine Job:

interface GitHubServiceApi {
    @GET("users/{login}")
    fun getUser(@Path("login") login: String): Deferred<User>
}

Note that Deferred is a subinterface of Job.

Then we need to add support Deferredfor , which requires the use of open source libraries:

implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2'

When constructing a Retrofit instance add:

val gitHubServiceApi by lazy {
    val retrofit = retrofit2.Retrofit.Builder()
            .baseUrl("https://api.github.com")
            .addConverterFactory(GsonConverterFactory.create())
            //添加对 Deferred 的支持
            .addCallAdapterFactory(CoroutineCallAdapterFactory())
            .build()

    retrofit.create(GitHubServiceApi::class.java)
}

Then when we initiate a request, we can write it like this:

GlobalScope.launch(Dispatchers.Main) {
    try {
        showUser(gitHubServiceApi.getUser("bennyhuo").await())
    } catch (e: Exception) {
        showError(e)
    }
}

Note: Dispatchers.MainThe implementation is different on different platforms, if it is on Android HandlerDispatcher, it is on Java Swing SwingDispatcherand so on .

First, we launchstart , which is similar to starting a thread launch. There are three parameters, which are coroutine context, coroutine startup mode, and coroutine body:

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext, // 上下文
    start: CoroutineStart = CoroutineStart.DEFAULT,  // 启动模式
    block: suspend CoroutineScope.() -> Unit // 协程体
): Job 

The startup mode is not a very complicated concept, but we don't care about it for the time being, and the default is to directly allow scheduling execution.

Context can have many functions, including carrying parameters, intercepting coroutine execution, etc. In most cases, we don't need to implement context ourselves, just use ready-made ones. An important function of the context is thread switching, Dispatchers.Mainwhich is an officially provided context, which can ensure that the coroutine body launchstarted runs in the UI thread (unless you launchperform thread switching within the coroutine body, or start and run in other Context coroutines with thread switching capabilities).

In other words, in the example, the entire launchinternal code you see runs on the UI thread. Although the thread is indeed switched getUserduring execution, it will be switched back again when the result is returned. This seems a little confusing, because intuition tells us that getUsera Deferredtype , and its awaitmethod will return an Userobject , which means that awaitit needs to wait for the request result to return before continuing to execute, so awaitwill it not block the UI thread?

The answer is: no. Of course not, Deferredotherwise Futurewhat is the difference between and ? Here is awaitvery suspicious, because it is actually a suspend function. This function can only be called inside the coroutine body or other suspend functions. It is like syntactic sugar for callbacks. It uses an Continuationinstance of the interface called return result:

@SinceKotlin("1.3")
public interface Continuation<in T> {
    public val context: CoroutineContext
    public fun resumeWith(result: Result<T>)
}

The source code for 1.3 is actually not very straightforward, although we could look at the source code Resultfor , but I don't want to do that. It is easier to understand the source code of the previous version:

@SinceKotlin("1.1")
public interface Continuation<in T> {
    public val context: CoroutineContext
    public fun resume(value: T)
    public fun resumeWithException(exception: Throwable)
}

I believe everyone can understand at once, this is actually a callback. If you still don't understand, then compare Retrofit's Callback:

public interface Callback<T> {
  void onResponse(Call<T> call, Response<T> response);
  void onFailure(Call<T> call, Throwable t);
}

When a result is returned normally, Continuationcall to resumereturn the result, otherwise call resumeWithExceptionto throw an exception, which is exactly Callbackthe same as .

So at this point you should understand that the execution flow of this code is essentially an asynchronous callback:

GlobalScope.launch(Dispatchers.Main) {
    try {
        //showUser 在 await 的 Continuation 的回调函数调用后执行
        showUser(gitHubServiceApi.getUser("bennyhuo").await())
    } catch (e: Exception) {
        showError(e)
    }
}

The reason why the code can look synchronous is the black magic of the compiler. Of course, you can also call it "syntactic sugar".

At this time, you may still have a problem: I didn’t see Continuationit , that’s right, this is exactly the black magic of the compiler we mentioned earlier. On the Java virtual machine, awaitthe signature of this method is not what we see. :

public suspend fun await(): T

Its real signature is actually:

kotlinx/coroutines/Deferred.await (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;

That is to receive an Continuationinstance and return Objectsuch a function, so we can roughly understand the previous code as:

//注意以下不是正确的代码,仅供大家理解协程使用
GlobalScope.launch(Dispatchers.Main) {
    gitHubServiceApi.getUser("bennyhuo").await(object: Continuation<User>{
            override fun resume(value: User) {
                showUser(value)
            }
            override fun resumeWithException(exception: Throwable){
                showError(exception)
            }
    })
}

And among awaitthem , roughly:

//注意以下并不是真实的实现,仅供大家理解协程使用
fun await(continuation: Continuation<User>): Any {
    ... // 切到非 UI 线程中执行,等待结果返回
    try {
        val user = ...
        handler.post{ continuation.resume(user) }
    } catch(e: Exception) {
        handler.post{ continuation.resumeWithException(e) }
    }
}

Everyone can understand such a callback at a glance. Having said so much, please remember one thing: From the perspective of execution mechanism, there is no essential difference between coroutines and callbacks.

4.2 The way of suspend function

The suspend function is the only black magic that the Kotlin compiler supports for coroutines (on the surface, there are other things we will talk about when we talk about the principle later). We have already got a general understanding of it through Deferredthe awaitWe Let's see how it can be used in Retrofit.

The current release version of Retrofit is 2.5.0, which does not yet support the suspend function. So if you want to try the following code, you need the latest Retrofit source code support; of course, maybe when you read this article, the new version of Retrofit already supports this feature.

First we modify the interface method:

@GET("users/{login}")
suspend fun getUser(@Path("login") login: String): User

In this case, Retrofit will construct according to the declaration of the interface method Continuation, and internally encapsulate the asynchronous request Callof (using enqueue), and then get Userthe instance. We will have the opportunity to introduce the specific principle later. The method of use is as follows:

GlobalScope.launch {
    try {
        showUser(gitHubServiceApi.getUser("bennyhuo"))
    } catch (e: Exception) {
        showError(e)
    }
}

Its execution process is Deferred.awaitsimilar to , so we won't analyze it in detail.

5. What exactly is a coroutine

Well, friends who insist on reading this far, you must be "victims" of asynchronous code, you must have encountered "callback hell", which makes your code readability drastically reduced; you have also written a lot of complex asynchronous logic Handling, exception handling, which increases your code repetition logic; because of the existence of callbacks, you have to deal with thread switching frequently, which seems not to be a difficult task, but as the code size increases, it will drive you crazy. There are not a few exceptions reported online due to improper use of threads.

The coroutine can help you handle these gracefully.

Coroutine itself is a concept that is separated from language implementation. We "very rigorously" (haha) give the definition of Wikipedia:

Coroutines are computer program components that generalize subroutines for non-preemptive multitasking, by allowing execution to be suspended and resumed. Coroutines are well-suited for implementing familiar program components such as cooperative tasks, exceptions, event loops, iterators, infinite lists and pipes.

To put it simply, a coroutine is a non-preemptive or cooperative implementation of concurrent scheduling of computer programs, and the program can actively suspend or resume execution. A little knowledge of the operating system is still required here. Most of the implementations of threads we recognize on the Java virtual machine are mapped to threads in the kernel, that is to say, the code logic in the thread grabs the time slice of the CPU when the thread grabs It can only be executed when it is executed, otherwise it has to rest. Of course, this is transparent to us developers; and we often hear that the so-called coroutine is lighter, which means that the coroutine will not be mapped into a kernel thread or other For such a heavy resource, its scheduling can be done in the user mode, and the scheduling between tasks is not preemptive, but collaborative.

Regarding concurrency and parallelism: Just because the CPU time slice is small enough, even a single-core CPU can give us the illusion that multiple tasks are running at the same time, which is the so-called "concurrency". Parallelism is true simultaneous operation. In terms of concurrency, it is more like Magic.

If you are familiar with the Java virtual machine, just imagine what the Thread class is, why does its run method run in another thread? Who is responsible for executing this code? Obviously, at first glance, Thread is actually just an object, and the run method contains the code to be executed - nothing more. The same is true for coroutines. If you only look at the API of the standard library, then it is too abstract, but we explained at the beginning that learning coroutines should not come up to touch the standard library. The kotlinx.coroutines framework is what our users should care about, and this The concept corresponding to Thread in the framework is Job. You can see its definition:

public interface Job : CoroutineContext.Element {
    ...
    public val isActive: Boolean
    public val isCompleted: Boolean
    public val isCancelled: Boolean

    public fun start(): Boolean
    public fun cancel(cause: CancellationException? = null)
    public suspend fun join()
    ...
}

Let's look at the definition of Thread again:

public class Thread implements Runnable {
    ...    
    public final native boolean isAlive();
    public synchronized void start() { ... }
    @Deprecated
    public final void stop() { ... }
    public final void join() throws InterruptedException  { ... }
    ...
}

Here we have omitted some comments and less relevant interfaces very intimately. We found that Thread and Job basically have the same function. They both carry a piece of code logic (the former uses the run method, and the latter uses the Lambda or function used to construct the coroutine), and both include the running status of the code.

In actual scheduling, there is an essential difference between the two. How to schedule specifically, we only need to know the scheduling results to use them well.

6. Summary

Let's introduce it through examples first, starting from the most familiar code to the example of coroutines, and then evolving to the writing method of coroutines, so that everyone can have a perceptual understanding of coroutines, and finally we give the definition of coroutines , and also tell everyone what the coroutine can do.

This article does not pursue any internal principles, but just tries to give everyone a first impression of how to use coroutines. If you still feel confused, don’t be afraid, I will use a few more articles to analyze the operation of the coroutine with examples, and the analysis of the principle will be discussed after everyone has mastered the coroutine.

If you need more Kotlin learning materials, you can scan the QR code to get them for free!

" The most detailed Android version of kotlin coroutine entry advanced combat in history "

Chapter 1 Introduction to the Basics of Kotlin Coroutines

​ ● What is a coroutine

​ ● What is Job, Deferred, and coroutine scope

​ ● Basic usage of Kotlin coroutines

img

Chapter 2 Preliminary Explanation of Key Knowledge Points of Kotlin Coroutine

​ ● Coroutine Scheduler

​ ● Coroutine context

​ ● Coroutine startup mode

​ ● Coroutine scope

​ ● suspend function

img

Chapter 3 Exception Handling of Kotlin Coroutines

​ ● Generation process of coroutine exception

​ ● Exception handling for coroutines

img

Chapter 4 Basic application of kotlin coroutines in Android

Guess you like

Origin blog.csdn.net/Android_XG/article/details/129798137