preface
This article mainly includes the following contents: 1. What is coroutine and the advantages of coroutine 2. The meaning and principle of the suspend keyword 3
What is a coroutine
Simply put, Coroutine is a concurrent design pattern that allows you to solve asynchronous problems with much cleaner code.
So it’s not just another manifestation of threads, as some people say. Although threads are also used inside coroutines. But it’s more important because of its design philosophy: eliminate our traditional Callback method and write asynchronous code in a synchronous way
The main advantages of coroutines
- 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.
What is a suspend
Suspend is the keyword for coroutines. Each method modified by suspend must be called in another suspend function or Coroutine program
Why is that?
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
Return value change
As you can see, the return value changes from NewsResponse to Any because when the suspend function is suspended by the 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.
Hang function decompilation is generated after the state machine, each starting point corresponding to the label, about bytecode compiled analytical details visible: Kotlin | 09 Jetpack practical experience. Graphic coroutine principles
Continuation parameters
Let’s take a look at the source code of the Continutation mentioned above
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, which is more of a CombinedContext type, and the coroutine like collection resumeWith is used to wake up a pending coroutine. 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. The coroutine uses resumeWith to replace the traditional callback. Each coroutine is created with the presence of Continutation, and the subsequent suspension function will wake up the coroutine and let it continue execution at the place where it was suspended.
What is a 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. CoroutineContext is flexible. If it needs to be changed, you can create a new CoroutineContext using the current CoroutineContext
First 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. Our common jobs, Dispatchers, and so on all implement the Element interface
public fun MainScope(a): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
Copy the code
Why can Job and Dispatchers be joined by + sign
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
Concatenation essentially utilizes the implementation logic of Kotlin’s custom plus operator to encapsulate two concatenated CoroutineContext into a CombinedContext to form a concatenated chain, ContinuationInterceptor is also added to the end of the splicing 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
When we look at the two arguments in the container, we’re going to run the container on the left side and the Dispatchers.Main side is going to be the element. With this foundation, we can look at its get method very clearly. So we can get the CoroutineContext instance of the Key from the CoroutineContext set in the entire coroutine directly by using a map-like method.
CoroutineScope
What is a CoroutineScope? If that sounds unfamiliar to you, GlobalScope, lifecycleScope, and viewModelScope are (for Android developers, of course). They all implement the CoroutineScope interface.
public interface CoroutineScope {
/** * The context of this scope. * Context is encapsulated by the scope and used for implementation of coroutine builders that are extensions on the scope. * Accessing this property in general code is not recommended for any purposes except accessing the [Job] instance for advanced usages. * * By convention, should contain an instance of a [job][Job] to enforce structured concurrency. */
public val coroutineContext: CoroutineContext
}
Copy the code
CoroutineScope contains only one variable to be implemented, CoroutineContext. By its structure, we can think of it as a container to provide CoroutineContext, ensuring that CoroutineContext can be passed through the entire coroutine operation. Constrain the bounds of CoroutineContext. For example, if you use a coroutine in Android to request data, the Activity exits before the interface completes the request, and not stopping the running coroutine can have unexpected consequences. Therefore, we recommend using viewModelScope to request coroutines, which will be automatically removed when the page is destroyed
How does viewModelScope implement automatic cancellation coroutine
How is a viewModelScope defined
val ViewModel.viewModelScope: CoroutineScope
get() {
val scope: CoroutineScope? = this.getTag(JOB_KEY)
if(scope ! =null) {
return scope
}
return setTagIfAbsent(JOB_KEY,
CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate))
}
internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
override val coroutineContext: CoroutineContext = context
override fun close(a) {
coroutineContext.cancel()
}
}
Copy the code
The viewModelScope is actually CloseableCoroutineScope and implements the Closeable and CoroutineScope interface
What happens when you close the page
final void clear(a) {
mCleared = true;
// Since clear() is final, this method is still called on mock objects
// and in those cases, mBagOfTags is null. It'll always be empty though
// because setTagIfAbsent and getTag are not final so we can skip
// clearing it
if(mBagOfTags ! =null) {
synchronized (mBagOfTags) {
for (Object value : mBagOfTags.values()) {
// see comment for the similar call in setTagIfAbsent
closeWithRuntimeException(value);
}
}
}
onCleared();
}
private static void closeWithRuntimeException(Object obj) {
if (obj instanceof Closeable) {
try {
((Closeable) obj).close();
} catch (IOException e) {
throw newRuntimeException(e); }}}Copy the code
You can see that when the page is closed, the clear method of the viewModel is automatically called, then the viewModelScope is pulled out by tag, and then the close method is called to unsubscribe
The resources
Kotlin Coroutine Kotlin coroutines principle: Suspend&CoroutineContext Kotlin | 09 Jetpack practical experience. This article will take you through Kotlin’s coroutines