Today we’re going to talk about Kotlin’s Coroutine.
If you haven’t been exposed to coroutines, I recommend reading this introductory article What? You don’t know Kotlin Coroutine?
If you’ve been around coroutines, you’ve probably had the following questions:
- What the hell is a coroutine?
- coroutines
suspend
What does it do? How does it work? - Some key names in coroutines (e.g.
Job
,Coroutine
,Dispatcher
,CoroutineContext
withCoroutineScope
) What is the relationship between them? - What is the so-called non-blocking suspend and resume of coroutines?
- What is the internal implementation of coroutines?
- .
The next few articles will try to analyze these questions, and you are welcome to join the discussion.
What is a coroutine
It’s a simple question, and you should know it if you’re not a stranger to coroutines. Because it’s clearly defined in the official documentation.
Here’s the official quote (and the most powerful part of the article).
Coroutines are a concurrent design pattern that you can use on the Android platform to simplify code that executes asynchronously.
Type on the blackboard: Coroutines are a concurrent design pattern.
So it’s not just another manifestation of threads, as some people say. Although threads are also used inside coroutines. But more important is its design philosophy. Eliminate our traditional Callback method. Approach asynchronous programming to synchronous alignment.
So with all that explained, let’s get straight to the point and look at the advantages
- Lightweight: You can run multiple coroutines on a single thread because coroutines support suspension and do not block the thread running them. Suspension saves memory than blocking and supports multiple parallel operations.
- Fewer memory leaks: Multiple operations are performed within a scope using structured concurrency mechanisms.
- Built-in cancellation support: Cancellation is automatically propagated through the running coroutine hierarchy.
- Jetpack Integration: Many Jetpack libraries include extensions that provide full coroutine support. Some libraries also provide their own coroutine scope that you can use to structure concurrency.
suspend
Suspend is the keyword for coroutines. Each method modified by suspend must be called in another suspend function or Coroutine program.
I don’t know if you have any questions about this definition for the first time, but I’m wondering why there should be a limit to the suspend modification method. Why not? What does it do?
Of course, if you’ve been following my previous posts, you should know this, because I’ve already mentioned it briefly in this post about reviewing Retrofit source code and laughing at coroutine implementations.
There is a mechanism involved commonly known as CPS(Continuation-passing Style). Every suspend decorated method or lambda expression adds an additional continuation-type argument to it when the code is called.
@GET("/v2/news")
suspend fun newsGet(@QueryMap params: Map<String, String>): NewsResponse
Copy the code
This is what this code really looks like after CPS conversion
@GET("/v2/news")
fun newsGet(@QueryMap params: Map<String, String>, c: Continuation<NewsResponse>): Any?
Copy the code
After the transformation, the old return type NewsResponse is added to the new Continutation parameter, and Any? Type. There might be a question here, right? If the return type is changed, the result is wrong.
Not really, Any? Special in Kotlin, it can represent any type.
When the suspend function is suspended by a coroutine, it returns a special identifier, COROUTINE_SUSPENDED, which is essentially an Any; When the coroutine executes without suspension, it returns the result of execution or the exception thrown. So in order to support returns in both cases, we use Kotlin’s unique Any, right? Type.
Now that the return value is clear, let’s talk about the Continutation parameter.
First look at the Continutation source code
public interface Continuation<in T> {
/**
* The context of the coroutine that corresponds to this continuation.
*/
public val context: CoroutineContext
/**
* Resumes the execution of the corresponding coroutine passing a successful or failed [result] as the
* return value of the last suspension point.
*/
public fun resumeWith(result: Result<T>)
}
Copy the code
Context is the context of a coroutine, and it’s more of a CombinedContext type, kind of like a collection of coroutines, and I’ll talk more about that later.
ResumeWith is used to wake up pending coroutines. This method is used to invoke the coroutine once the logic inside the coroutine has finished executing. Let it continue where it was suspended.
So each function decorated by suspend takes the upper Continutation and passes it to itself as a parameter. Since it is passed from the top, who creates Continutation?
In fact, it is not hard to guess that Continutation is created at the same time that coroutines are created.
GlobalScope.launch {
}
Copy the code
The Continutation object was created and the coroutine started at launch. So the arguments that the suspended coroutine passes in it are all this object.
The simple understanding is that the coroutine uses resumeWith to replace the traditional callback. The creation of each coroutine will be accompanied by the existence of Continutation, and the resumeWith method of Continutation will be called automatically when the coroutine is created, so that the coroutine can start to execute.
CoroutineContext
The context of a coroutine, which contains user-defined sets of data that are closely related to the coroutine. It is similar to a map collection and can be used to retrieve different types of data through keys. At the same time, CoroutineContext is very flexible. If it needs to change, it only needs to use the current CoroutineContext to create a new CoroutineContext.
Let’s look at the definition of CoroutineContext
public interface CoroutineContext { /** * Returns the element with the given [key] from this context or `null`. */ public operator fun <E : Element> get(key: Key<E>): E? /** * Accumulates entries of this context starting with [initial] value and applying [operation] * from left to right to current accumulator value and each element of this context. */ public fun <R> fold(initial: R, operation: (R, Element) -> R): R /** * Returns a context containing elements from this context and elements from other [context]. * The elements from this context with the same key as in the other one are dropped. */ public operator fun plus(context: CoroutineContext): CoroutineContext = ... /** * Returns a context containing elements from this context, but without an element with * the specified [key]. */ public fun minusKey(key: Key<*>): CoroutineContext /** * Key for the elements of [CoroutineContext]. [E] is a type of element with this key. */ public interface Key<E : Element> /** * An element of the [CoroutineContext]. An element of the coroutine context is a singleton context by itself. */ public interface Element : CoroutineContext {.. }}Copy the code
Each CoroutineContext has a unique Key of type Element that we can use to retrieve the object. It’s a little abstract and let’s just go straight to the example.
var context = Job() + Dispatchers.IO + CoroutineName("aa") LogUtils.d("$context, ${context[CoroutineName]}") context = context.minuskey (Job) logutils. d("$context") // JobImpl{Active}@158b42c, CoroutineName(aa), LimitingDispatcher@aeb0f27[dispatcher = DefaultDispatcher]], CoroutineName(aa) [CoroutineName(aa), LimitingDispatcher@aeb0f27[dispatcher = DefaultDispatcher]]Copy the code
Job, Dispatchers, and CoroutineName all implement the Element interface.
If you need to combine different CoroutineContext, you can simply concatenate the CoroutineContext, essentially using the Plus method.
public operator fun plus(context: CoroutineContext): CoroutineContext =
if (context === EmptyCoroutineContext) this else // fast path -- avoid lambda creation
context.fold(this) { acc, element ->
val removed = acc.minusKey(element.key)
if (removed === EmptyCoroutineContext) element else {
// make sure interceptor is always last in the context (and thus is fast to get when present)
val interceptor = removed[ContinuationInterceptor]
if (interceptor == null) CombinedContext(removed, element) else {
val left = removed.minusKey(ContinuationInterceptor)
if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
CombinedContext(CombinedContext(left, element), interceptor)
}
}
}
Copy the code
The implementation logic of Plus is to wrap two concatenated CoroutineContext into the CombinedContext to form a concatenated chain, while adding ContinuationInterceptor to the end of the concatenated chain each time.
So what is a CombinedContext?
internal class CombinedContext( private val left: CoroutineContext, private val element: Element ) : CoroutineContext, Serializable { override fun <E : Element> get(key: Key<E>): E? { var cur = this while (true) { cur.element[key]? .let { return it } val next = cur.left if (next is CombinedContext) { cur = next } else { return next[key] } } } ... }Copy the code
Notice its two parameters, and let’s go straight to the example above
Job() + Dispatchers.IO
(Job, Dispatchers.IO)
Copy the code
Job corresponds to left, dispatchers. IO to element. CoroutineName(AA) looks like this if you splice another layer
((Job, Dispatchers.IO),CoroutineName)
Copy the code
The function is similar to that of a linked list, but the difference is that you can retrieve the entire content linked to you. The corresponding minusKey method removes the CoroutineContext instance for the corresponding Key from the collection.
With this foundation, we can look at its get method very clearly. Fetch from element first, not from left.
So what is this Key? Let’s take a look at CoroutineName
public data class CoroutineName(
/**
* User-defined coroutine name.
*/
val name: String
) : AbstractCoroutineContextElement(CoroutineName) {
/**
* Key for [CoroutineName] instance in the coroutine context.
*/
public companion object Key : CoroutineContext.Key<CoroutineName>
/**
* Returns a string representation of the object.
*/
override fun toString(): String = "CoroutineName($name)"
}
Copy the code
The Key is coroutinecontext. Key
. This is not enough. We need to combine the operator get method with the operator get method
public override operator fun <E : Element> get(key: Key<E>): E? =
@Suppress("UNCHECKED_CAST")
if (this.key == key) this as E else null
Copy the code
Kotlin’s operator overload feature is used here. Then the following code is equivalent.
context.get(CoroutineName)
context[CoroutineName]
Copy the code
Therefore, we can directly obtain the CoroutineContext instance of the corresponding Key in the CoroutineContext set in the whole coroutine in a way similar to Map.
This article introduces the operation of suspend and the internal structure of CoroutineContext. Hope to learn coroutine partners can be helpful, please look forward to the subsequent coroutine analysis.
project
Android_startup: provides a simple and efficient way to initialize components during application startup, optimizing startup speed. Not only does it support all the features of Jetpack App Startup, but it also provides additional synchronous and asynchronous waiting, thread control, and multi-process support.
AwesomeGithub: Based on Github client, pure exercise project, support componentized development, support account password and authentication login. Kotlin language for development, the project architecture is based on Jetpack&DataBinding MVVM; Popular open source technologies such as Arouter, Retrofit, Coroutine, Glide, Dagger and Hilt are used in the project.
Flutter_github: a cross-platform Github client based on Flutter, corresponding to AwesomeGithub.
Android-api-analysis: A comprehensive analysis of Knowledge points related to Android with detailed Demo to help readers quickly grasp and understand the main points explained.
Daily_algorithm: an algorithm of the day, from shallow to deep, welcome to join us.