Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”

This article also participated in the “Digitalstar Project” to win a creative gift package and creative incentive money

📚 If you are a beginner to Coroutines on the Android platform, check out the previous article: The Android Coroutines Series: Getting Started

❓ Where does the Android coroutine use context?

The globalScope. launch constructor requires a context to be passed in. If not, the default is EmptyCoroutineContext.

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope. () - >Unit
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}
Copy the code

The only difference between scope and context is the purpose for which it is used

  • Scopes are used to manage coroutines;
  • The context is simply a collection of records of the environment in which the coroutine runs

🍈 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

@SinceKotlin("1.3")
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 =
        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)
                    }
                }
            }

    /** * 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 {
        /** * A key of this coroutine context element. */
        public val key: Key<*>

        public override operator fun <E : Element> get(key: Key<E>): E? =
            @Suppress("UNCHECKED_CAST")
            if (this.key == key) this as E else null

        public override fun <R> fold(initial: R, operation: (R.Element) - >R): R =
            operation(initial, this)

        public override fun minusKey(key: Key< * >): CoroutineContext =
            if (this.key == key) EmptyCoroutineContext else this}}Copy the code

Each CoroutineContext has a unique Key of type Element that we can use to retrieve the object. See by example

fun main(a) {
    var context = Job() + Dispatchers.IO + CoroutineName("Name")

    println("$context")
    println("${context[CoroutineName]}")

    context = context.minusKey(Job)

    println("minusKey:$context")}Copy the code

The output

[JobImpl{Active}@6193b845, CoroutineName(name), dispatchers. IO] CoroutineName(name) minusKey:[CoroutineName(name), Dispatchers.IO]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.

/** * 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 =
    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.

translation

/** * merge the two contexts, replace them with the one on the right if there are duplicates, and second, make sure the interceptor is in the last position ** /
 
public operator fun plus(context: CoroutineContext): CoroutineContext =
 
        if (context === EmptyCoroutineContext) {
 
            // fast path -- avoid lambda creation
 
            // If you want to merge an empty context, return the current context
 
            this
 
        } else {
 
            // If both contexts have content
 
            context.fold(this) { acc, element ->
 
                Acc.minuskey calculates the content of the left context after removing the key
 
                val removed = acc.minusKey(element.key)
 
                if (removed === EmptyCoroutineContext) {
 
                    // If there is an empty context on the left, there is only one key on the left
 
                    // So, replace the left with the right, and return the right element
 
                    element
 
                } else {
 
                    // If the left context has its own content, the next step is to merge the two contexts,
 
                    // Replace the same key with the one on the right
 
                    // make sure interceptor is always last in the context (and thus is fast to get when present)
 
                    // Make sure the interceptor is always last in the context (so it can be quickly retrieved when it appears)
 
                    // It feels like the context is already a CombinedContext
 
                    val interceptor = removed[ContinuationInterceptor]
 
                    if (interceptor == null) {
 
                        // If there is no interceptor on the left, merge the left and right
 
                        CombinedContext(removed, element)//A
 
                    } else {
 
                        // If there is an interceptor on the left, fetch the other context first
 
                        val left = removed.minusKey(ContinuationInterceptor)
 
                        if (left === EmptyCoroutineContext) {
 
                            // If the other context is empty, then there is only one interceptor left on the left
 
                            CombinedContext(element, interceptor)//B: pay attention to the parameter position, compare with A
 
                        } else {
 
                            // If there are other contexts on the left side besides the interceptor, the other context should be merged with the right side to ensure that the interceptor is at the end,
 
                            // Finally merge interceptors to the right
 
                            CombinedContext(CombinedContext(left, element), interceptor)
 
                        }
 
                    }
 
                }
 
            }
 
        }
Copy the code

So what is a CombinedContext?

@SinceKotlin("1.3")
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

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(a): 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

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.