The author

Hello everyone, my name is 🐜

I joined the Android team of 37 Mobile Games in October 2020

At present, I am mainly responsible for domestic business development and some daily business

background

Kotlin coroutine use for a period of time, the common usage has been very familiar, but are staying in the use of the stage, not to deeply understand the code, or feel a little virtual; Taking advantage of the New Year this period of time, for the coroutine exception handling, the relevant source code to learn a wave, comb summarize their own understanding.

This paper is based on Kotlin V1.4.0, Kotlin-Coroutines V1.3.9 source analysis

1, CoroutineScope source analysis

What it does: creates and tracks coroutines, manages parent-child relationships between different coroutines, and structures the way coroutines are created:

1. Create via CoroutineScope

2. Create in coroutines

In the first way, how do you create it through CoroutineScope in the first place?

val scope = CoroutineScope(Job() + Dispatchers.Main) 
Copy the code
@Suppress("FunctionName") public fun CoroutineScope(context: CoroutineContext): CoroutineScope = ContextScope(if (context[Job] ! = null) else context + Job()Copy the code
internal class ContextScope(context: CoroutineContext) : CoroutineScope {
    override val coroutineContext: CoroutineContext = context
}
Copy the code

CoroutineScope is a global method in which a CoroutineScope object is instantiated by ContextScope. Similar to MainScope or Android platform viewModelScope and lifecycleScope(but in the lifecycle related callback do some automatic cancel processing) are also run here. In addition, when the scope is initialized, it will generate a job, which will act as a trace. Note the difference between GlobalScope and CoroutineScope. The GlobalScope Job is empty because its coroutineContext is EmptyCoroutineContext and has no Job

Once we have scope, we can create a coroutine via launch

val job = scope.launch {}
Copy the code

Check out the stamp code

public fun CoroutineScope.launch( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = Coroutinestart. DEFAULT, block: suspend CoroutineScope.() -> Unit): Job {... Omit the code... return coroutine }Copy the code

There are three launch parameters, the first two of which are analyzed, and the third is a lambda parameter with a “receiver” (see what “receiver” is in Kotlin). The default type is CoroutineScope

Val job = scope. Launch {①/* this: CoroutineScope */ // The new coroutine will have CoroutineScope as its parent, created in launch. // Since launch is an extension method, the default receiver in the example above is this. CoroutineScop launch {/*... */} this.launch {// create a new coroutine as the parent of the current coroutine}}Copy the code

Take a look at the implementation of Coroutinescope.launch

public fun CoroutineScope.launch( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> Unit ): Job {// create a new context based on the parent (the coroutine's parent context), and then create a coroutine for the following, Val newContext = newCoroutineContext(context) val Coroutine = if (start.islazy) LazyStandaloneCoroutine(newContext, block) else StandaloneCoroutine(newContext, block) Start (start, coroutine, block) return coroutine}Copy the code
public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext {/ / it is a CoroutineScope extension function, CoroutineContext is actually getting to the members of the object scope, and then through the "+" can make a, We're going to say "+" and we're going to think of it as adding a context to a context map data group, and we're going to make some logical judgments, but anyway, Val debug = if (debug) combined + val debug = if (debug) combined + CoroutineId(COROUTINE_ID.incrementAndGet()) else combined return if (combined ! == Dispatchers.Default && combined[ContinuationInterceptor] == null) debug + Dispatchers.Default else debug }Copy the code

How do the pluses add up? This refers to the related class CoroutineContext: the interface to all contexts CombinedContext: the class generated when contexts are combined coroutinecontext.element: Most single-context implementation classes, as some will directly implement CoroutineContext

public operator fun plus(context: CoroutineContext): CoroutineContext = //operator overload eg:Job() + Dispatchers.IO + CoroutineName("test") if (context === EmptyCoroutineContext) this else path -- avoid lambda creation Element is the addend 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

Can be thought of as a map (actually a single linked list, see how the Kotlin CoroutineContext context can be added) to retrieve different types of data by key, To change the CoroutineContext, create a new CoroutineContext using the current CoroutineContext. Val scope = CoroutineScope(Job() + dispatchers.main)

Combining the above two code fragments, we can see that a new coroutine CoroutineContext consists of elements

1. There is an element, job, that controls the life cycle of the coroutine

The remaining elements inherit from the CoroutineContext’s parent, which may be another coroutine or the CoroutineScope that created the coroutine

2. Type of CoroutineScope

2.1. Influence of coroutine scope on anomaly propagation

Type:

Function analysis:

Description:

When abnormal c2-1 occurs, c2-1 ->C2->C2-2->C2->C1->C3 (including subcoroutines) ->C4

When an exception occurs at C3-1-1, c3-1-1 ->C3-1-1-1, and other parameters are not affected

When c3-1-1-1 is abnormal, c3-1-1-1 ->C3-1-1, and other parameters are not affected

2.2 schematic code

1. C1 has nothing to do with C2

C1 globalscope.launch {// coroutine C2 //... }}Copy the code

2, C2, and C3 are subcoroutines of C1, and exceptions to C2 and C3 cancel C1

Globalscope.launch {// coroutineScoope {launch{}// coroutine C2 launch{}// coroutine C3}}Copy the code

3. C2 and C3 are subcoroutines of C1, and C2 and C3 exceptions do not cancel C1

Globalscope.launch {// coroutine C1 supervisorScope {launch{}// coroutine C2 {}// coroutine C3}}Copy the code

Take 🌰 for example

Eg1:

@Test
fun test()= runBlocking{
    val handler = CoroutineExceptionHandler { coroutineContext, exception ->
        println("CoroutineExceptionHandler got $exception  coroutineContext ${coroutineContext}")
    }
    val job = GlobalScope.launch(handler) {
        println("1")
        delay(1000)
        coroutineScope {
            println("2")
            val job2 = launch(handler) {
                throwErrorTest()
            }
            println("3")
            job2.join()
            println("4")
        }
    }
    job.join()

}
fun throwErrorTest(){
    throw Exception("error test")
}
Copy the code

Output results:

If the coroutine in which joB2 is located has an exception, the job is cancelled (” 4 “is not printed), and the exception is thrown from the coroutine where the job is located

eg2:

@Test
fun test()= runBlocking{
    val handler = CoroutineExceptionHandler { coroutineContext, exception ->
        println("CoroutineExceptionHandler got $exception  coroutineContext ${coroutineContext}")
    }
    val job = GlobalScope.launch(handler) {
        println("1")
        delay(1000)
        supervisorScope {
            println("2")
            val job2 = launch(handler) {
                throwErrorTest()
            }
            println("3")
            job2.join()
            println("4")
        }
    }
    job.join()

}
fun throwErrorTest(){
    throw Exception("error test")
}
Copy the code

Output results:If the job2 coroutine is in the master-slave scope, the exception does not cancel the job (” 4 “is printed), and the exception is thrown by the job2 coroutine

3, coroutine exception handling process source analysis

3.1, three layers of coroutine packaging

The first layer: the job returned by launch and async encapsulates the state of the coroutine and provides an interface for fetching coroutine. The instances are inherited from AbstractCoroutine

Level 2: The compiler generates (CPS) a subclass of SuspendLambda that wraps the coroutine’s real operation logic, inherited from the BaseContinuationImpl, where the Completion attribute is the first wrapper for the coroutine

The third layer: DispatchedContinuation, which encapsulates the thread scheduling logic, contains the second wrapper of the coroutine, and the third wrapper implements the Continuation interface, combining the layers of the coroutine through the proxy pattern, Each layer is responsible for a different function and the logic runs at the invokeSuspend in the resumeWith() function of the BaseContinuationImpl at the second layer

3.2. Entry where an exception occurs

BaseContinuationImpl resumeWith(result: result <Any? >) handles exception logic, omits part of the code

public final override fun resumeWith(result: Result) { val completion = completion!! // Fail fast when trying to resume continuation without completion val outcome: Result =... Other code... try { val outcome = invokeSuspend(param) if (outcome === COROUTINE_SUSPENDED) return Result.success(outcome) } catch (exception: Throwable) {result.failure (exception)}... Other code... Completion. ResumeWith (outcome)... Other code... }Copy the code

It can be seen from the above code analysis

1, the implementation of the invokeSuspend(param) method is generated in the compilation, corresponding to the coroutine body processing logic

2. When an exception occurs, i.e., outcome is result.failure (exception), the specific call is made in complet.resumewith (outcome). Through AbstractCoroutine. ResumeWith (Result. Failure (exception)) into the third layer in the packaging

Continue to follow AbstractCoroutine. ResumeWith (result: the result) – > JobSupport. MakeCompletingOnce (proposedUpdate: Any?) : Any? -> JobSupport.tryMakeCompleting(state: Any? , proposedUpdate: Any?) : Any? ->JobSupport.tryMakeCompletingSlowPath(state: Incomplete, proposedUpdate: Any?) : Any?

In tryMakeCompletingSlowPath approach

var notifyRootCause: Throwable? = null synchronized(finishing) {//... Other code... notifyRootCause = finishing.rootCause.takeIf { ! wasCancelling } } // process cancelling notification here -- it cancels all the children _before_ we start to to wait them (sic!!!) // In this case, notifyRootCause is an exception notifyRootCause? .let { notifyCancelling(list, it) } // otherwise -- we have not children left (all were already cancelled?) Return finalizeFinishingState(Finishing, proposedUpdate) //... Other code...Copy the code

If an exception occurs that notifyRootCause is not empty, the notifyCancelling method is called, primarily to cancel the subcoroutine

private fun notifyCancelling(list: NodeList, cause: Throwable) {
    // first cancel our own children
    onCancelling(cause)
    notifyHandlers>(list, cause)
    // then cancel parent
    cancelParent(cause) // tentative cancellation -- does not matter if there is no parent
}
Copy the code

The other method, finalizeFinishingState, is mainly the logic for exception passing and handling. The key code is as follows

private fun finalizeFinishingState(state: Finishing, proposedUpdate: Any?) : Any? {... Other code... // Now handle the final exception if (finalException ! = null) {// If cancelParent(finalException) does not handle exceptions, HandleJobException (finalException) is handled by the current // coroutine. Will be mentioned below) val handled = cancelParent (finalException) | | handleJobException (finalException) if (handled) (finalState as CompletedExceptionally). MakeHandled ()}. Other code... return finalState }Copy the code
/** * The method that is invoked when the job is cancelled to possibly propagate cancellation to the parent. * Returns `true` if the parent is responsible for handling the exception, `false` otherwise. * * Invariant: never returns `false` for instances of [CancellationException], Otherwise to exception * may leak to the [CoroutineExceptionHandler]. * returns refers to is true, by the parent coroutines processing, Private fun cancelParent(cause: Throwable): Boolean { // Is scoped coroutine -- don't propagate, will be rethrown /** * Returns `true` for scoped coroutines. * Scoped coroutine is a coroutine that is executed sequentially within the enclosing scope without any concurrency. * Scoped coroutines always handle any exception happened within -- they just rethrow it to the enclosing scope. * Examples of scoped coroutines are `coroutineScope`, */ // if isScopedCoroutine is true, that is, if coroutineScope is a master-slave scope, If (isScopedCoroutine) return true if (isScopedCoroutine) return true if (isScopedCoroutine) return true CancellationException is considered "normal" and parent usually is not cancelled when child produces it. * This allow parent to cancel its children (normally) without being cancelled itself, unless * child crashes and produce some other exception during its completion. */ val isCancellation = cause is CancellationException val parent = parentHandle // No parent -- ignore CE, report other exceptions. if (parent === null || parent === NonDisposableHandle) { return isCancellation } // Notify Parent but don't forget to check cancellation //childCancelled(cause) Supervisorjobs and supervisorScopes are used when they are contained. the parent coroutine is not handled when the child coroutine catches an uncaught exception. // Their principle is to rewrite childCancelled() as Override fun childCancelled(cause: Throwable): Boolean = false return parent.childCancelled(cause) || isCancellation }Copy the code

You can see from the above code

1. When an uncaught exception occurs, all subcoroutines are first cancelled

2. The parent coroutine is not cancelled when the exception is a CancellationException

The SupervisorJob is designed to run on the supervisorScope, and it is designed to run on the supervisorScope. The SupervisorJob is designed to run on the supervisorScope, and it is designed to run on the supervisorScope. The SupervisorJob is designed to run on the supervisorScope, and it is designed to run on the supervisorScope

3.3, CoroutineExceptionHandler effect how

In AbstractCoroutine, the logic for handling exceptions is in the JobSupport interface, which defaults to an empty implementation. protected open fun handleJobException(exception: Throwable): Boolean = false The specific implementation logic is in StandaloneCoroutine (Builders.common.kt file).

private open class StandaloneCoroutine( parentContext: CoroutineContext, active: Boolean ) : AbstractCoroutine(parentContext, active) { override fun handleJobException(exception: Throwable): Boolean {// handleCoroutineException(context, exception) return true}}Copy the code

The specific implementation is as follows

//CoroutineExceptionHandlerImpl.kt private val handlers: List = ServiceLoader.load( CoroutineExceptionHandler::class.java, CoroutineExceptionHandler::class.java.classLoader ).iterator().asSequence().toList() internal actual fun handleCoroutineExceptionImpl(context: CoroutineContext, exception: Throwable) { // use additional extension handlers for (handler in handlers) { try { handler.handleException(context, exception) } catch (t: Throwable) { // Use thread's handler if custom handler failed to handle exception val currentThread = Thread.currentThread() currentThread.uncaughtExceptionHandler.uncaughtException(currentThread, handlerException(exception, }} // use thread's handler val currentThread = thread.currentThread () currentThread.uncaughtExceptionHandler.uncaughtException(currentThread, exception) }Copy the code

The above processing logic can be easily summarized as the following pseudocode

class StandardCoroutine(context: CoroutineContext) : AbstractCoroutine(context) { override fun handleJobException(e: Throwable): Boolean { context[CoroutineExceptionHandler]? .handleException(context, e) ? : Thread.currentThread().let { it.uncaughtExceptionHandler.uncaughtException(it, e) } return true } }Copy the code

So by default, launch type coroutines for an uncaught exception is just print the exception stack information, if you use the CoroutineExceptionHandler, can only use a custom exception handling CoroutineExceptionHandler.

summary

1. The default scope of coroutines is the cooperative scope, and exceptions are propagated to the parent coroutine handler of the form coroutineScope or coroutineScope (Job()).

It is designed to run in the format of the CoroutineScope(supervisorScope), and it is designed to run in the format of the CoroutineScope(SupervisorJob()). The key is to overwritten childCancelled()=false.

3, coroutines handle exceptions, if custom CoroutineExceptionHandler, by its processing, otherwise to system.

Finally, the exception handling analysis in this article is carried out from the coroutine scope as the pointcut, and you will learn some clever use of kotlin syntax as you look at the code. In addition, only a general analysis of the main logic of exception processing, some details need to continue to learn, next time will be more detailed analysis, I hope this article is helpful to you, also welcome to communicate with you to learn.

Welcome to communicate

Students who have problems or need to communicate with each other in the process can scan the QR code and add friends, and then enter the group to communicate with each other about problems and technologies.