1. Introduction

This is the first article since the New Year, here to wish you all a happy old age, wish you in the New Year can achieve what you want, and thank you all for your support and help all the time. In fact, this article was finished before the Spring Festival. I had planned to finish it during the Spring Festival in Beijing, but due to the change of plan, I went back to my hometown for the Spring Festival. During the Spring Festival, I spent most of my time visiting relatives, so the article was shelved.

Without further ado, this article will try to show you how the Kotlin coroutine implements asynchronous calls in a synchronous manner. I believe many students can name one or more of the following concepts.

  • Kotlin suspend keyword
  • Kotlin’s internal Continuation mechanism
  • Continuation Passing Style (CPS) mechanism
  • Finite state machine mechanism

If you’ve watched the video of Kotlin’s Deep Dive into Coroutines on JVM presentation, you’ll be familiar with the above concepts.

Video link 👉www.youtube.com/watch?v=Yrr…

But if you haven’t delved into the source code to see how it works, you probably don’t know much about the above concepts. This article will take you through this knowledge in more detail.

2. A step-by-step example

Imagine a simple scenario where you make a request on your App, get the response 10 seconds later, and update it to the user interface. There are several possible ways to write this.

2.1 Call directly from the main thread

In this case, as we all know, it is a no-no for client development to perform time-consuming operations on the main thread. In order not to block the main thread, we need to start a new thread and use a callback to pass back the result.

2.2 use callback

This way, while it solves the problem of doing time-consuming operations on the main thread, it introduces a new problem: updating the UI on the child thread can cause the application to crash. To solve this problem, we need to run callback posts to the main thread through the main thread Handler.

2.3 Using callback and Handler

So far, we’ve done a pretty good job of executing an asynchronous request and updating the results to the UI using a combination of threads, callbacks, and handlers.

There are three elements in this plan:

1. Thread or thread pool technology

2. Callback mechanism that feeds results back to the caller

3. The thread in which the callback operation is executed


So it’s time to type on the board and highlight it

Since this article is about coroutines, which classes in coroutines correspond to each of these three elements?

  1. Dispatchers.Main, dispatchers. IO and other corresponding threads

  2. Continuations correspond to callbacks in a thread

  3. DispatchedContinuation specifies both the Continuation callback and the thread in which the callback is executed.

I won’t go into the details of continuations and DispatchedContinuations here, but I’ll cover them later.

2.4 Use coroutines

This code is the correct code for the coroutine implementation, there is no problem with the main thread performing time-consuming operations, and there is no problem with child threads updating the UI. There’s no explicit callback, no code for thread switching. Simply by adding the suspend keyword to the suspendHeavyWork method, asynchronous calls can be implemented in synchronous code. So where’s the magic?

3. Magic revealed

First post the full code

Use Android Studio’s Show Kotlin Bytecode feature to view the decompiled file

When x1 looks at the decompiled code, it might be worth noting that there is no such thing as peace and quiet, but someone is carrying a heavy load for you. Writing code with coroutines is fun because the compiler does a lot of work behind The Times, and understanding what goes on behind The Times helps us use coroutines.

The code generated by the compiler looks both familiar and unfamiliar, familiar because every line of code is known syntactically (switch case is familiar), unfamiliar because in general, I don’t quite understand what they are doing, and some functions (like invokeSuspend) don’t even know where they are called.

Confused x2 To make things even more confusing, there is a return statement in the state machine code. How does the subsequent code execute? To understand how a state machine works you have to understand this problem.

4. Research launch decompilation

The figure above highlights three important areas.

  1. Lambda expressions are converted to Function2 instances, so what is Function2? Why pass a NULL object of type Continuation to Function2?

  2. What does the invokeSuspend method do? Note that the parameter is non-null.

  3. The call to heavyWork(this) passed this object. As we saw earlier, the heavyWork method was decompiled to heavyWork(Continuation VAR). So Function2 is a Continuation type.

With these questions in mind, the answer is not far off.

4.1 invokeSuspend

To answer the simplest question first, what does the invokeSuspend method do? It is defined in the BaseContinuationImpl class and is an abstract method.

From BaseContinuationImpl (public val completion: Continuation < Any? >? We can see that the callbacks in coroutines are linked by a list.

Suppose you have a suspend function call like this, then the Continuation diagram looks like this.

  1. First, a while loop, which ensures that the state machine can poll.
  2. val completion = completion!! If there are no callbacks left of the current Continuation, return quickly
  3. Val outcome = invokeSuspend(Param) calls the invokeSuspend method of the current Continuation
  4. Returns directly if outcome === COROUTINE_SUSPENDED. This is why the delay method does not block the current thread; the suspend method label +1 is encountered, and the current Continuation is passed to delay.
  5. If (completion is BaseContinuationImpl) continues the recursive call to invokeSuspend
  6. Otherwise, call completion. ResumeWith (outcome) and return

This code is at the heart of what keeps the state machine running.

4.2 What is Function2?

Function2 is SuspendLambda, defined in ContinuationImp.kt. It is a subclass of BaseContinuationImpl.

5. DispatchedContinuation

internal class DispatchedContinuation<in T>(
    @JvmField val dispatcher: CoroutineDispatcher,
    @JvmField val continuation: Continuation<T>
)
Copy the code

DispatchedContinuation has two member variables: dispatcher and Continuation. The continuation callback is executed on the corresponding thread of the Dispatcher. When a thread switch occurs, a DispatchedContinuation object must be generated; otherwise, after the thread is cut, it cannot be cut back.

For example, with Delay and withContext, a DispatchedContinuation is created after a thread is switched to record the thread on which the callback is being called.

Welcome to pay attention to the “byte station” public account of the same name!! Welcome to pay attention to the “byte station” public account of the same name!! Welcome to pay attention to the “byte station” public account of the same name!!