At the heart of Kotlin Coroutines is the CoroutineContext interface. All coroutine generator functions such as launch and Async have the same first argument, context: CoroutineContext. All coroutine builders are defined as extension functions of the CoroutineScope interface, which has an abstract read-only property coroutineContext: coroutineContext.

Each Coroutine Builder is an extension of the CoroutineScope and inherits its coroutineContext to automatically pass and cancel context elements.

  • launch
fun CoroutineScope.launch(
  context: CoroutineContext = EmptyCoroutineContext, 
  start: CoroutineStart = CoroutineStart.DEFAULT, 
  block: suspend CoroutineScope.() -> Unit
): Job (source)
Copy the code
  • async
fun <T> CoroutineScope.async(
  context: CoroutineContext = EmptyCoroutineContext, 
  start: CoroutineStart = CoroutineStart.DEFAULT, 
  block: suspend CoroutineScope.() -> T
): Deferred<T> (source)
Copy the code

CoroutineContext is a basic building block of Kotlin Coroutines. Therefore, it is critical to be able to manipulate it in order to achieve the correct behavior of threads, life cycles, exceptions, and debugging.

CoroutineContext creates a set of elements that define the behavior of coroutines. It is a data structure that encapsulates key information about coroutine execution. It contains the following sections:

  • Job: Handle to the life cycle of the coroutine
  • CoroutineDispatcher
  • CoroutineName: The name of the coroutine
  • CoroutineExceptionHandler: coroutines exception handling

When an exception occurs in coroutines, if an exception is not processing, at the same time CoroutineExceptionHandler has not been set, the exception will be distributed to the JVM ExceptionHandler, in Android, If you do not set the global ExceptionHandler, the App will Crash.

CoroutineContext data structure

Take a look at the CoroutineContext data structure.

CoroutineContext is an index set of element instances that, in terms of data structure, is a hybrid of set and Map. Each element in this collection has a unique Key, which is compared by reference.

The API of the CoroutineContext interface may seem obscure at first, but it is really just a type-safe, heterogeneous map that compares coroutinecontext.key instances (by reference rather than by value, according to the class’s documentation, that is, Element instance (this Map is made up of different types of elements whose index is key).

To understand why a new interface must be redefined rather than simply using a standard Map, we can refer to a similar equivalent declaration like the one below.

typealias CoroutineContext = Map<CoroutineContext.Key<*>, CoroutineContext.Element>
Copy the code

In this case, the GET method cannot infer the type of element obtained from the Key being used, even though this information is actually available in the Key generics.

fun get(key: CoroutineContext.Key<*>): CoroutineContext.Element?
Copy the code

Therefore, every time an element is retrieved from the Map, it needs to be converted to the actual type. In the CoroutineContext class, the more general get method actually defines the returned Element type based on the generic type of the Key passed as an argument.

fun <E : Element> get(key: Key<E>): E?
Copy the code

In this way, elements can be safely retrieved without casting because their type is specified in the Key used. So, in a real CoroutineContext, the get function retrieves the Element of type Element in the CoroutineContext by Key. The caller can retrieve an element of type Key by CoroutineContext[Key], similar to retrieving an element with index from a List — List[index].

Since the Key is static in CoroutineContext, multiple instances share the same Key, so only one element of the same type exists in the Map, making all instances unique.

For example, to obtain the CoroutineName of a coroutine, do the following.

coroutineContext[CoroutineName]
Copy the code

The coroutineContext property is a parameter generated by Kotlin at compile time, which passes the current coroutineContext to each suspend function so that the current coroutine information can be retrieved directly from the CoroutineScope.

But there’s something weird here, in order to find a CoroutineName, we just use CoroutineName. It’s not a type, it’s not a class, it’s a companion object. It is a feature of Kotlin that the name of a class itself can be used as a reference to its associated object, so coroutineContext[CoroutineName] is just a shorthand for coroutineContext[coroutinename.key].

So, actually, the original way to write it would be like this.

coroutineContext[object : CoroutineContext.Key<CoroutineName> {}]
Copy the code

In CoroutineName’s class, we found this code.

public companion object Key : CoroutineContext.Key<CoroutineName>
Copy the code

It is this companion object that makes it easy to refer to.

coroutineContext[CoroutineName.Key] ------> coroutineContext[CoroutineName]
Copy the code

This technique is used a lot in coroutine libraries.

The + operator

CoroutineContext does not implement a collection interface, so it has no typical collection-related operators. But it overrides an important operator, the plus operator.

The plus operator combines CoroutineContext instances with each other. It merges the elements they contain, overwriting the elements in the context to the left with elements in the context to the right of the operator, much like the behavior on a Map.

The plus operator returns a context containing elements from this context and elements from other contexts. Elements in this context that have the same Key value as in another context are deleted.

The CoroutineContext.Element interface actually inherits CoroutineContext. This is handy because it means that coroutinecontext. Element instances can simply be treated as CoroutineContext containing a single Element, that is, themselves.

An element of a Coroutine context is itself a context containing only itself.

With this “+” operator, you can easily combine elements and between elements into a new context. Note the order in which they are combined, because the + operator is asymmetric.

The EmptyCoroutineContext object can be used in cases where a context does not need to hold any elements. As you can expect, adding this object to any other context has no effect on that context.

Take the following example.

(Dispatchers.Main, “name”) + (Dispatchers.IO) = (Dispatchers.IO, “name”)
Copy the code

Note that a new coroutine context, in addition to the context inherited from the parent coroutine, must have a newly created Job object that controls the life cycle of the coroutine.

The nice thing about CoroutineContext adding elements this way is that when you add elements, the resulting CombinedContext also inherits from CoroutineContext so that when you use coroutine constructor functions such as launch, CoroutineContext can be passed as a single CoroutineContext, or a combination of CoroutineContext can be passed as a “+” without using the list or vararg arguments.

As we can see from the above figure, CoroutineContext is actually immutable, and every time a “+” operation is performed, a new CombinedContext (which is also an implementation of CoroutineContext) is generated, and the CombinedContext, Is the true implementer of CoroutineContext.

CombinedContext

Let’s look at the CombinedContext declaration.

internal class CombinedContext(
    private val left: CoroutineContext,
    private val element: Element
) : CoroutineContext, Serializable {
Copy the code

Left, element, old drivers will know at a glance, naked linked list.

In terms of inheritance, CombinedContext, CoroutineContext and Element are all CoroutineContext.

Since CoroutineContext has a limited number of elements in Kotlin, the performance of this data structure is expected.

Let’s take a look at the plus operator using the CombinedContext, what it’s doing, and summarize the code.

First of all, after plus, it will return the CombinedContext, if anything, containing only objects on both sides of plus, and there are two possibilities, one is that when A + B, B has the same Key as A, then the corresponding Key in A gets deleted, and the Key in B is used, otherwise, it’s just chained.

So, looking for Element in the CombinedContext becomes, if Element in the CombinedContext (i.e. the current node) contains the corresponding Key, then return, otherwise continue recursion from left, so, In CombinedContext, the order of traversal recurses from right to left.

In addition, there is a special type of Element called ContinuationInterceptor, which is always placed at the very end for easy traversal. It’s a natural choice.

Elements

As explained earlier, CoroutineContext is essentially a Map that always holds a predefined Set. Because all keys must implement the Coroutinecontext.key interface, it is easy to find a list of common elements by searching the code for the Coroutinecontext.key implementation and checking which element class they are associated with. Elements implementation classes are basically the following: ContinuationInterceptor, Job, CoroutineExceptionHandler and CoroutineName, i.e. CoroutineContext essentially will only have this several types of elements.

  • ContinuationInterceptor is called for continuations to manage the underlying thread of execution. In practice, ContinuationInterceptor always inherits from the CoroutineDispatcher base class.
  • The Job holds a handle to the lifecycle and task hierarchy of the executing Coroutine.
  • CoroutineExceptionHandler spread by those who don’t abnormal coroutine builder (that is, the launch and the actor) is used, in order to determine what to do when a in case of abnormal.
  • CoroutineName is usually used for debugging.

Each Key is defined as a companion object to its associated element interface or class. In this way, the Key can be referenced directly by using the name of the element type. For example, coroutineContext[Job] will return an instance of the Job held by coroutineContext, or null if it contains no instance.

Without extensibility, CoroutineContext could even be defined simply as a class.

class CoroutineContext( val continuationInterceptor: ContinuationInterceptor? , val job: Job? , val coroutineExceptionHandler: CoroutineExceptionHandler, val name: CoroutineName? )Copy the code

Coroutine scope builder

Each CoroutineScope will have a coroutineContext property that lets us retrieve the Element Set of the current Coroutine.

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
}

lifecycleScope.launch {
    println("My context is: $coroutineContext")
}
Copy the code

When we want to start a Coroutine, we need to call a builder function on a CoroutineScope instance. In the builder function, we actually see three contexts at work.

  • The CoroutineScope receiver is defined by the way it provides the CoroutineContext, which is the context of inheritance.
  • The builder function receives an instance of CoroutineContext in its first argument, which we call a context parameter.
  • The pause block argument in the builder function has a CoroutineScope receiver, which itself also provides a CoroutineContext, which is the context of the Coroutine.

Take a look at the source code for launch and Async; they both start with the same statement.

val newContext = newCoroutineContext(context)
Copy the code

The newCoroutineContext extension function on CoroutineScope handles the merging of inherited context and context parameters, as well as providing default values and doing some additional configuration. The merge is written as coroutineContext + context, where coroutineContext is the inherited context and context is the context parameter. Considering what was explained earlier about the coroutinecontext. plus operator, the operator on the right takes precedence, so properties from the context parameter override properties in the inherited context. The result is what we call a parent context.

parent context = default values + inherited context + context argument

The instance of CoroutineScope that is passed to kickbacks as a receiver is actually coroutine itself, always inheriting AbstractCoroutine, which implements CoroutineScope and is also a Job. The Coroutine context is provided by the class and returns the previously obtained parent context, which adds itself to the class, effectively overwriting the Job.

coroutine context = parent context + coroutine job

CoroutineContext default values

Some of the four basic elements of CoroutineContext have Default values, such as Dispatchers for CoroutineDispatcher and CoroutineName for Coroutine.

When an element is missing from a context being used by coroutine, it uses a default value.

  • ContinuationInterceptor’s Default value is dispatchers.default. This is documented in newCoroutineContext. Therefore, if neither the inherited context nor the context parameter has a Dispatcher, the default dispatcher is used. In this case, the Coroutine Context will also inherit the default Dispatcher.
  • If the context has no Job, the coroutine being created has no parent.
  • If no CoroutineExceptionHandler context, then can use global exception handler (but not in context). This will eventually call handleCoroutineExceptionImpl, it first USES Java ServiceLoader to load all CoroutineExceptionHandler implementation, abnormal and then transmitted to the current thread uncaught exception handler. On Android, a special exception handler is called AndroidExceptionPreHandler automatically, to report to the Thread on the hidden uncaughtExceptionPreHandler attribute is unusual, but it will cause the application to crash, Record exceptions to terminal logs.
  • The default name of a coroutine is “coroutine”, and the Key CoroutineName is used to get the name from the context.

Look at the way the proposed CoroutineScope is assumed to be a class that can be implemented by adding it to the default value.

val defaultExceptionHandler = CoroutineExceptionHandler { ctx, t ->
  ServiceLoader.load(
    serviceClass, 
    serviceClass.classLoader
  ).forEach{
    it.handleException(ctx, t)
  }
  Thread.currentThread().let { 
    it.uncaughtExceptionHandler.uncaughtException(it, exception)
  }
}

class CoroutineContext(
  val continuationInterceptor: ContinuationInterceptor =
    Dispatchers.Default,
  val parentJob: Job? = 
    null,
  val coroutineExceptionHandler: CoroutineExceptionHandler = 
    defaultExceptionHandler, 
  val name: CoroutineName = 
    CoroutineName("coroutine")
)
Copy the code

The sample

With some examples, let’s look at the context generated in some coroutine expressions, most importantly what dispatchers and parent jobs they inherit.

Global Scope Context

GlobalScope.launch {
  /* ... */
}
Copy the code

If we look at GlobalScope’s source code, we’ll see that its implementation of coroutineContext always returns an EmptyCoroutineContext. Therefore, the final context used in this coroutine will use all the default values.

For example, the above statement is the same as the following statement, except that the default dispatcher is explicitly specified in the following code.

GlobalScope.launch(Dispatchers.Default) { 
  /* ... */
}
Copy the code

Fully Qualified Context

Conversely, we can override the default implementation by passing all parameters to our own Settings.

coroutineScope.launch( Dispatchers.Main + Job() + CoroutineName("HelloCoroutine") + CoroutineExceptionHandler { _, _ - > / *... */}) {/*... * /}Copy the code

Any element in the inherited context is effectively overwritten, which has the advantage that the statement behaves the same regardless of which CoroutineScope is invoked.

CoroutineScope Context

In the Coroutines UI programming guide for Android, we found the following examples in the structured Concurrency, Lifecycle, and Coroutine parent-child hierarchies that show how to implement CoroutineScope in an Activity.

abstract class ScopedAppActivity: AppCompatActivity() { private val scope = MainScope() override fun onDestroy() { super.onDestroy() scope.cancel() } /* . * /}Copy the code

In this example, the MainScope helper factory function is used to create a scope with a predefined UI Dispatcher and Supervisor job. This is a design choice so that all Coroutine builders called on this scope will use the Main Dispatcher instead of Default.

Defining elements in the context of a scope is a way to override the library’s default values where context is used. This scope also provides a job, so all coroutines started from this scope have the same parent. Thus, there is a single point to cancel them, which is tied to the Activity’s life cycle.

Overriding Parent Job

We can combine the two by having some context elements inherited from the scope and others added in context parameters. For example, when NonCancellable job is used, it is usually the only element in the context passed as an argument.

withContext(NonCancellable) {
    /* ... */
}
Copy the code

The code executing in this block inherits the Dispatcher from the context in which it calls, but it overwrites the Job of that context by using NonCancellable as the parent. This way, the coroutine will always be active.

Binding to Parent Job

When launch and Async are used as extension functions to the CoroutineScope, Elements (including job) in the scope are automatically inherited. However, when using CompletableDeferred (which is a useful tool), you can bind a callback-based API to a Coroutine, whose parent job needs to be provided manually.

val call: Call
val deferred = CompletableDeferred<Response>()
call.enqueue(object: Callback {
  override fun onResponse(call: Call, response: Response) {
    completableDeferred.complete(response)
  }

  override fun onFailure(call: Call, e: IOException) {
    completableDeferred.completeExceptionally(e)
  }
})
deferred.await()
Copy the code

This type of architecture makes it easier to wait for the result of the call. However, because of the structured concurrency of coroutines, deferred can cause memory leaks if it cannot be cancelled. So, the easiest way to ensure that the CompletableDeferred is cancelled correctly is to bind it to its parent job.

val deferred = CompletableDeferred<Response>(coroutineContext[Job])
Copy the code

Accessing Context Elements

Elements in the current context can be captured using the read-only attribute of the coroutineContext function for top-level Suspending.

println("Running in ${coroutineContext[CoroutineName]}")
Copy the code

For example, the above statement could be used to print the name of the current coroutine.

If we wish, we can actually recreate a coroutine context from a single element that is identical to the current context.

val inheritedContext = sequenceOf(
  Job, 
  ContinuationInterceptor, 
  CoroutineExceptionHandler, 
  CoroutineName
).mapNotNull { key -> coroutineContext[key] }
  .fold(EmptyCoroutineContext) { ctx: CoroutineContext, elt -> 
    ctx + elt
  }
launch(inheritedContext) {
  /* ... */
}
Copy the code

Although interesting for understanding what constitutes context, this example is completely useless in practice. We can obtain exactly the same behavior by leaving the context parameter for startup as the default null value.

Nested Context

The last example is important because it illustrates the behavior changes in the latest version of Coroutines, where the builder function becomes an extension of the CoroutineScope.

GlobalScope.launch(Dispatchers.Main) {
  val deferred = async {
    /* ... */
  } 
  /* ... */
}
Copy the code

Since async is called on the scope (and not a top-level function), it inherits the scope’s Dispatcher, which is designated dispatchers.main instead of using the default dispatcher. In previous versions of Coroutines, the code in Async would run on the worker thread provided by dispatchers.default, but now it will run on the UI thread, which can cause the application to block or even crash.

The solution is to be more explicit about the dispatcher used in Async.

launch(Dispatchers.Main) {
  val deferred = async(Dispatchers.Default) {
    /* ... */
  } 
  /* ... */
}
Copy the code

Coroutine API Design

Coroutine apis are designed to be flexible and expressive. By combining contexts with the simple + operator, language designers can easily define the properties of coroutines when they start them and inherit them from the execution context. This gives developers full control over their coroutines while keeping the syntax smooth.

Reference links: proandroiddev.com/demystifyin…

I would like to recommend my website xuyisheng. Top/focusing on Android-Kotlin-flutter welcome you to visit