Because the article involves only a lot of points, the content may be too long, according to their own ability level and familiarity with the stage jump read. If there is an incorrect place to tell you trouble private letter to the author, very grateful
In order to facilitate reading, this article is divided into multiple chapters, and the corresponding chapters are selected according to their own needs. Now it is only a general catalog in the author’s mind, and the final update shall prevail:
- Basic usage of Kotlin coroutines
- Kotlin coroutine introduction to Android version in detail (ii) -> Kotlin coroutine key knowledge points preliminary explanation
- Kotlin coroutine exception handling
- Use Kotlin coroutine to develop Android applications
- Network request encapsulation for Kotlin coroutines
- Kotlin coroutine theory (1)
- Kotlin coroutine theory (2)
- [Kotlin coroutine introduction for Android version in detail (8) -> In-depth Kotlin coroutine principle (3)]
- [Kotlin coroutine introduction for Android (9) -> In-depth Kotlin coroutine principle (4)]
Extend the series
- Encapsulating DataBinding saves you thousands of lines of code
- A ViewModel wrapper for everyday use
This chapter, the preface
In the previous section, we looked at what coroutines actually do and what the suspend suspend function produces at compile time. In this chapter, we will mainly explain the implementation details of coroutine execution, suspension, recovery and state machine.
The little secrets of coroutines
Let’s create a simple test function to simulate a network request three times:
fun test(a) {
GlobalScope.launch {
Log.d("test"."Network Request 1")
delay(1000)
Log.d("test"."Network Request 2")
delay(1000)
Log.d("test"."Network Request 3")
delay(1000)
Log.d("test"."end")}}Copy the code
From the previous section we learned about the bytecode generated by the suspended function when compiled. Now we’ll focus on the Java code generated by test:
public final void test(a) {
BuildersKt.launch$default((CoroutineScope)GlobalScope.INSTANCE, (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {
int label;
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
label26: {
Object var2;
label25: {
var2 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch(this.label) {
case 0:
ResultKt.throwOnFailure($result);
Log.d("test"."Network Request 1");
this.label = 1;
if (DelayKt.delay(1000L.this) == var2) {
return var2;
}
break;
case 1:
ResultKt.throwOnFailure($result);
break;
case 2:
ResultKt.throwOnFailure($result);
break label25;
case 3:
ResultKt.throwOnFailure($result);
break label26;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
Log.d("test"."Network Request 2");
this.label = 2;
if (DelayKt.delay(1000L.this) == var2) {
return var2;
}
}
Log.d("test"."Network Request 3");
this.label = 3;
if (DelayKt.delay(1000L.this) == var2) {
return var2;
}
}
Log.d("test"."end");
return Unit.INSTANCE;
}
public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
Intrinsics.checkNotNullParameter(completion, "completion");
Function2 var3 = new <anonymous constructor>(completion);
return var3;
}
public final Object invoke(Object var1, Object var2) {
return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE); }}),3, (Object)null);
Copy the code
There’s a lot of code, but don’t worry, let’s break it down one by one. The builderskt. launch$default function is called only in the test function.
We know that the launch mode is an extension of the CoroutineScope method defined in KT file Builders. So this is kind of easy to understand.
@file:JvmName("BuildersKt")
package kotlinx.coroutines
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
But the launch function only has three arguments, so why does it pass in four at once?
Note that since the extension function is kotlin’s own, when converted to Java, he adds an extension object argument to the first one by default, such as:
class Test {
class User
fun User.test(a){}}Copy the code
After compiling to Java:
public final class Test {
public final void test(@NotNull Test.User $this$test) {
Intrinsics.checkNotNullParameter($this$test, "$this$test");
}
public static final class User {}}Copy the code
Back to business! From the previous chapter, we know the generation process of the fourth parameter coroutine body Function2. How does the coroutine work
The creation process of coroutines
It can be seen that LazyStandaloneCoroutine is derived from StandaloneCoroutine when the start of coroutine is passed in the launch function, and they are finally derived from AbstractCoroutine. Similarly, starting coroutines with Async eventually inherits AbstractCoroutine.
private class LazyStandaloneCoroutine(
parentContext: CoroutineContext,
block: suspend CoroutineScope.() -> Unit
) : StandaloneCoroutine(parentContext, active = false) {
private val continuation = block.createCoroutineUnintercepted(this.this)
override fun onStart(a) {
continuation.startCoroutineCancellable(this)}}private open class StandaloneCoroutine(
parentContext: CoroutineContext,
active: Boolean
) : AbstractCoroutine<Unit>(parentContext, initParentJob = true, active = active) {
/ /...
}
private class LazyDeferredCoroutine<T>(
parentContext: CoroutineContext,
block: suspend CoroutineScope.() -> T
) : DeferredCoroutine<T>(parentContext, active = false) {
private val continuation = block.createCoroutineUnintercepted(this.this)
override fun onStart(a) {
continuation.startCoroutineCancellable(this)}}private open class DeferredCoroutine<T>(
parentContext: CoroutineContext,
active: Boolean
) : AbstractCoroutine<T>(parentContext, true, active = active), Deferred<T>, SelectClause1<T> {
/ /...
}
Copy the code
The thing that we’re going to notice here is that the Lazy starting coroutine object they’re overwriting the onStart function, just to make it easier to call it later. The coroutine created by default does not override the onStart function, but starts directly at creation time.
So let’s go ahead and look at AbstractCoroutine implementation, and I’m just going to post some of the things that we’re going to focus on this time, but there are other things that we’re going to focus on as well, but we’ll cover that in the future.
public abstract class AbstractCoroutine<in T>(
parentContext: CoroutineContext,
initParentJob: Boolean,
active: Boolean
) : JobSupport(active), Job, Continuation<T>, CoroutineScope {
/ /...
public fun <R> start(start: CoroutineStart, receiver: R, block: suspend R. () - >T) {
start(block, receiver, this)}}Copy the code
AbstractCoroutine start function AbstractCoroutine start function AbstractCoroutine start function AbstractCoroutine start function AbstractCoroutine start function AbstractCoroutine The third block is the body of the coroutine we need to execute.
Here we continue to look at the call to the start method, which needs to be particularly important. If you press CTRL+ left mouse button directly on Android Studio, you will always be positioned in the Start parameter. Gives you a false sense of what’s going on in a loop. (Very serious to tell you: don’t ask me how to know, I will not tell you, my mouse all points rotten!!) . It is actually the invoke function of CoroutineStart.
public enum class CoroutineStart {
/ /...
public operator fun <T> invoke(block: suspend() - >T, completion: Continuation<T>): Unit =
when (this) {
DEFAULT -> block.startCoroutineCancellable(completion)
ATOMIC -> block.startCoroutine(completion)
UNDISPATCHED -> block.startCoroutineUndispatched(completion)
LAZY -> Unit // will start lazily}}public operator fun <R, T> invoke(block: suspend R. () - >T, receiver: R, completion: Continuation<T>): Unit =
when (this) {
DEFAULT -> block.startCoroutineCancellable(receiver, completion)
ATOMIC -> block.startCoroutine(receiver, completion)
UNDISPATCHED -> block.startCoroutineUndispatched(receiver, completion)
LAZY -> Unit // will start lazily}}Copy the code
You can see that the function start in AbstractCoroutine calls the second invoke function. We know the first two parameters. The third parameter, completion, is a Continuation, created in launch (keep that completion in mind), and invoked when the coroutine completes with a result or exception.
As we learned in the previous chapter, the block also carries a Continuation, which is the equivalent of two continuations, but they serve different purposes. Read on:
public fun <R, T> (suspend R.() -> T).startCoroutine(
receiver: R,
completion: Continuation<T>
) {
createCoroutineUnintercepted(receiver, completion).intercepted().resume(Unit)}internal fun <R, T> (suspend (R) -> T).startCoroutineCancellable(
receiver: R, completion: Continuation<T>,
onCancellation: ((cause: Throwable) -> Unit)? = null
) =
runSafely(completion) {
createCoroutineUnintercepted(receiver, completion).intercepted().resumeCancellableWith(Result.success(Unit), onCancellation)
}
internal fun <R, T> (suspend (R) -> T).startCoroutineUndispatched(receiver: R, completion: Continuation<T>) {
startDirect(completion) { actualCompletion ->
withCoroutineContext(completion.context, null) {
startCoroutineUninterceptedOrReturn(receiver, actualCompletion)
}
}
}
Copy the code
StartCoroutine () function and startCoroutineCancellable, they all call createCoroutineUnintercepted function, it is compiled, Including the startCoroutineUninterceptedOrReturn startCoroutineUndispatched function we have no way to directly view on the AS, need to check the source code to see.
public actual inline fun <R, T> (suspend R.() -> T).startCoroutineUninterceptedOrReturn(
receiver: R,
completion: Continuation<T>
): Any? = (this asFunction2<R, Continuation<T>, Any? >).invoke(receiver, completion)internal actual inline fun <R, P, T> (suspend R.(P) -> T).startCoroutineUninterceptedOrReturn(
receiver: R,
param: P,
completion: Continuation<T>
): Any? = (this asFunction3<R, P, Continuation<T>, Any? >).invoke(receiver, param, completion)public actual fun <T> (suspend(a) -> T).createCoroutineUnintercepted(
completion: Continuation<T>
): Continuation<Unit> {
val probeCompletion = probeCoroutineCreated(completion)
return if (this is BaseContinuationImpl)
create(probeCompletion)
else
createCoroutineFromSuspendFunction(probeCompletion) {
(this asFunction1<Continuation<T>, Any? >).invoke(it) } }public actual fun <R, T> (suspend R.() -> T).createCoroutineUnintercepted(
receiver: R,
completion: Continuation<T>
): Continuation<Unit> {
val probeCompletion = probeCoroutineCreated(completion)
return if (this is BaseContinuationImpl)
create(receiver, probeCompletion)
else {
createCoroutineFromSuspendFunction(probeCompletion) {
(this asFunction2<R, Continuation<T>, Any? >).invoke(receiver, it) } } }Copy the code
Does this feel like deja vu to the code? As mentioned in the previous section, suspended functions are eventually converted to a SuspendLambda type when compiled, which inherits from BaseContinuationImpl and implements the FunctionN family of functions.
So we know startCoroutineUninterceptedOrReturn finally calls the invoke, is actually SuspendLambda invoke the function, it internally call the create function to create, The call to invokeSuspend then executes the coroutine.
public final Object invokeSuspend(@NotNull Object $result) {
/ /...
}
public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
Intrinsics.checkNotNullParameter(completion, "completion");
Function2 var3 = new <anonymous constructor>(completion);
return var3;
}
public final Object invoke(Object var1, Object var2) {
return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
}
Copy the code
It is now clear that var1, the first parameter to Invoke, is the coroutine scope receiver, and var2, the second parameter, is completion, the coroutine body we created. The second SuspendLambda creation is then done through create.
There is one more detail to note here. If we create a child coroutine from within the parent coroutine, SuspendLambda will add a variable, and the create method will assign the coroutine scope var1 to this variable, and the invokeSuspend function will be nested. Such as:
fun test(a) {
GlobalScope.launch {
launch { }
}
}
Copy the code
The Java code we see compiled will look like this:
public final void test(a) {
BuildersKt.launch$default((CoroutineScope)GlobalScope.INSTANCE, (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {
private Object L$0;
int label;
@Nullable
public final Object invokeSuspend(@NotNull Object var1) {
/ /...
CoroutineScope $this$launch = (CoroutineScope)this.L$0;
BuildersKt.launch$default($this$launch, (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {
/ /...}}@NotNull
public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
Intrinsics.checkNotNullParameter(completion, "completion");
Function2 var3 = new <anonymous constructor>(completion);
var3.L$0 = value;
return var3;
}
public final Object invoke(Object var1, Object var2) {
return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE); }}),3, (Object)null);
}
Copy the code
You can see that the outer SuspendLambda adds an L$0 object and that the outer create function assigns value to L$0. The outer invokeSuspend function then passes this L$0 to the child coroutine, and if the child coroutine creates a grandchild coroutine, it will continue recursively (see for yourself).
Suddenly a little bit too much code, is it not acceptable. It’s okay. Let’s take the heat out of it.
Back to us before this topic, startCoroutineUninterceptedOrReturn by calling SuspendLambda invoke the function, and call the create function and invokeSuspend performed internally coroutines. And how did the createCoroutineUnintercepted perform coroutines.
public actual fun <R, T> (suspend R.() -> T).createCoroutineUnintercepted(
receiver: R,
completion: Continuation<T>
): Continuation<Unit> {
val probeCompletion = probeCoroutineCreated(completion)
return if (this is BaseContinuationImpl)
create(receiver, probeCompletion)
else {
createCoroutineFromSuspendFunction(probeCompletion) {
(this asFunction2<R, Continuation<T>, Any? >).invoke(receiver, it) } } }Copy the code
A probe is created to intercept the completion of the coroutine through the probeCoroutineCreated function. Once created, the coroutine is repeatedly restored and suspended until it is complete. The object returned by the probe is called when the coroutine completes.
+-------+ probeCoroutineCreated +-----------+ | START | ---------------------->| SUSPENDED | +-------+ +-----------+ | ^ probeCoroutineResumed | | probeCoroutineSuspended +-------+ | | | | | V | | +------------+ completion invoked +-----------+ +-- | RUNNING | ------------------->| COMPLETED | +------------+ +-----------+Copy the code
Keep reading. If the caller is already a BaseContinuationImpl, calling create to create returns a Continuation, which is the final implementation SuspendLambda.
From the previous chapter, we learned that the coroutine ultimately executes the SuspendLambda, the base Connection Configuration Impl concrete implementation. If the caller is not BaseContinuationImpl, then we need some other way to wrap it as a BaseContinuationImpl.
Here is through createCoroutineFromSuspendFunction for packaging. Interested can look at the source code. I only show the key information here.
private inline fun <T> createCoroutineFromSuspendFunction(
completion: Continuation<T>,
crossinline block: (Continuation<T- > >)Any?).: Continuation<Unit> {
val context = completion.context
return if (context === EmptyCoroutineContext)
object : RestrictedContinuationImpl(completion asContinuation<Any? >) {/ / /...
}
else
object : ContinuationImpl(completion asContinuation<Any? >, context) {/ /...}}Copy the code
If the context is EmptyCoroutineContext coroutines, will get a RestrictedContinuationImpl, it is also a BaseContinuationImpl subclass, However, unlike ContinuationImpl, which is context-specific, it also adds interceptor functionality, as we mentioned in the previous chapter.
internal abstract class RestrictedContinuationImpl( completion: Continuation<Any? >? : BaseContinuationImpl(completion) {init{ completion? .let { require(it.context === EmptyCoroutineContext) {"Coroutines with restricted suspension must have EmptyCoroutineContext"}}}public override val context: CoroutineContext
get() = EmptyCoroutineContext
}
internal abstract class ContinuationImpl( completion: Continuation<Any? >? .private val _context: CoroutineContext?
) : BaseContinuationImpl(completion) {
constructor(completion: Continuation<Any?>?) : this(completion, completion? .context)public override val context: CoroutineContext
get() = _context!!
@Transient
private varintercepted: Continuation<Any? >? =null
public fun intercepted(a): Continuation<Any? > = intercepted ? : (context[ContinuationInterceptor]? .interceptContinuation(this) ?: this)
.also { intercepted = it }
protected override fun releaseIntercepted(a) {
/ /..}}Copy the code
Either way, they ultimately return a BaseContinuationImpl to be executed by an external call.
public fun <R, T> (suspend R.() -> T).startCoroutine(
receiver: R,
completion: Continuation<T>
) {
createCoroutineUnintercepted(receiver, completion).intercepted().resume(Unit)}internal fun <R, T> (suspend (R) -> T).startCoroutineCancellable(
receiver: R, completion: Continuation<T>,
onCancellation: ((cause: Throwable) -> Unit)? = null
) =
runSafely(completion) {
createCoroutineUnintercepted(receiver, completion).intercepted().resumeCancellableWith(Result.success(Unit), onCancellation)
}
Copy the code
It then calls either resume or resumeCancellableWith, but they both end up calling resumeWith.
public inline fun <T> Continuation<T>.resume(value: T): Unit =
resumeWith(Result.success(value))
public fun <T> Continuation<T>.resumeCancellableWith(
result: Result<T>,
onCancellation: ((cause: Throwable) - >Unit)? = null
): Unit = when (this) {
is DispatchedContinuation -> resumeCancellableWith(result, onCancellation)
else -> resumeWith(result)
}
Copy the code
This time does not seem to feel a little bit wrong. Why by startCoroutineUninterceptedOrReturn created is through a call to invoke, and call the create function to create within it calls when invokeSuspend performed coroutines. But by createCoroutineUnintercepted through resumeWith function to perform.
Suspension and recovery of coroutines
At this point, we need to go back to the source of BaseContinuationImpl and look at the resumeWith function.
internal abstract class BaseContinuationImpl(
public val completion: Continuation<Any?>?
) : Continuation<Any?>, CoroutineStackFrame, Serializable {
public final override fun resumeWith(result: Result<Any? >) {
var current = this
var param = result
while (true) {
// Invoke "resume" debug probe on every resumed continuation, so that a debugging library infrastructure
// can precisely track what part of suspended callstack was already resumed
probeCoroutineResumed(current)
with(current) {
val completion = completion!! // fail fast when trying to resume continuation without completion
valoutcome: Result<Any? > =try {
val outcome = invokeSuspend(param)
if (outcome === COROUTINE_SUSPENDED) return
Result.success(outcome)
} catch (exception: Throwable) {
Result.failure(exception)
}
releaseIntercepted() // this state machine instance is terminating
if (completion is BaseContinuationImpl) {
// unrolling recursion via loop
current = completion
param = outcome
} else {
// top-level completion reached -- invoke and return
completion.resumeWith(outcome)
return}}}}protected abstract fun invokeSuspend(result: Result<Any? >): Any?
/ /...
}
Copy the code
In the resumeWith function, the execution of the coroutine is done in a while(true) loop, followed by the probeCoroutineResumed function to probe the execution of the coroutine. Each time the coroutine resumes a portion of the call stack that was stored in the heap, the subprobe is called. To track exactly which part of the suspended call stack has been recovered.
Then call invokeSuspend to continue the execution, if the coroutine execution state is equal to COROUTINE_SUSPENDED then the subsequent execution will be cancelled by return, until call resumeWith to resume the coroutine execution.
If the execution state is not COROUTINE_SUSPENDED, determine whether the body completion is BaseContinuationImpl. If so, you need to continue loop execution until loop execution reaches the top-level completion, and then return the result via resumeWith.
COROUTINE_SUSPENDED is a value of enumeration type that has no concrete meaning, merely indicating that the coroutine has been suspended and does not immediately return any results.
public val COROUTINE_SUSPENDED: Any get() = CoroutineSingletons.COROUTINE_SUSPENDED
internal enum class CoroutineSingletons { COROUTINE_SUSPENDED, UNDECIDED, RESUMED }
Copy the code
So we can think of invokeSuspend as the place where the code inside the coroutine body is actually executed, and resumeWith as the recovery execution after the coroutine has been suspended.
So here’s the question:
- The coroutine executes first
invokeSuspend
orresumeWith
? - How many times will the coroutine be executed during the entire execution
invokeSuspend
? - Does the coroutine have to hang?
- How does coroutine execution continue after an exception occurs?
Let’s take a snippet of invokeSuspend after the original test function is compiled into Java code to see how the coroutine is restored via resumeWith:
public final Object invokeSuspend(@NotNull Object $result) {
label26: {
Object var2;
label25: {
var2 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch(this.label) {
case 0:
ResultKt.throwOnFailure($result);
Log.d("test"."Network Request 1");
this.label = 1;
if (DelayKt.delay(1000L.this) == var2) {
return var2;
}
break;
/ /...
}
/ /...
Log.d("test"."end");
return Unit.INSTANCE;
}
Copy the code
The delaykt. delay function is executed in case0, returning the value to resumeWith. Let’s look at the implementation:
public suspend fun delay(timeMillis: Long) {
if (timeMillis <= 0) return
return suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->
if (timeMillis < Long.MAX_VALUE) {
cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
}
}
}
public suspend inline fun <T> suspendCancellableCoroutine(
crossinline block: (CancellableContinuation<T- > >)Unit
): T =
suspendCoroutineUninterceptedOrReturn { uCont ->
val cancellable = CancellableContinuationImpl(uCont.intercepted(), resumeMode = MODE_CANCELLABLE)
cancellable.initCancellability()
block(cancellable)
cancellable.getResult()
}
override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
val block = Runnable {
with(continuation) { resumeUndispatched(Unit)}}if (handler.postDelayed(block, timeMillis.coerceAtMost(MAX_DELAY))) {
continuation.invokeOnCancellation { handler.removeCallbacks(block) }
} else {
cancelOnRejection(continuation.context, block)
}
}
Copy the code
Can see call suspendCancellableCoroutine function in delay, put up the current coroutines, then execute the block, namely scheduleResumeAfterDelay method, Resume is performed by creating a Runnable object, and the result is returned by cancellable.getresult.
internal fun getResult(a): Any? {
val isReusable = isReusable()
if (trySuspend()) {
if (parentHandle == null) {
installParentHandle()
}
if (isReusable) {
releaseClaimedReusableContinuation()
}
return COROUTINE_SUSPENDED
}
if (isReusable) {
releaseClaimedReusableContinuation()
}
val state = this.state
if (state is CompletedExceptionally) throw recoverStackTrace(state.cause, this)
if (resumeMode.isCancellableMode) {
val job = context[Job]
if(job ! =null && !job.isActive) {
val cause = job.getCancellationException()
cancelCompletedResult(state, cause)
throw recoverStackTrace(cause, this)}}return getSuccessfulResult(state)
}
private fun trySuspend(a): Boolean {
_decision.loop { decision ->
when (decision) {
UNDECIDED -> if (this._decision.compareAndSet(UNDECIDED, SUSPENDED)) return true
RESUMED -> return false
else -> error("Already suspended")}}}override fun <T> getSuccessfulResult(state: Any?).: T =
when (state) {
is CompletedContinuation -> state.result as T
else -> state as T
}
Copy the code
The first attempt in getResult is to suspend the execution of the Continuation while returning COROUTINE_SUSPENDED. TrySuspend may fail if the Continuation has been restored or cancelled. The result value is then returned if everything is completed properly. It is important to note _decision is UNDECIDED in CancellableContinuationImpl defined by default.
+-----------+ trySuspend +-----------+
| UNDECIDED | -------------> | SUSPENDED |
+-----------+ +-----------+
|
| tryResume
V
+-----------+
| RESUMED |
+-----------+
private val _decision = atomic(UNDECIDED)
Copy the code
Execution of coroutines and state machines
A simple example shows how coroutines can be restored. Let’s move on to the execution of coroutines. So let’s go back to our original example. Now that we know what the Create and invoke functions do, let’s focus on invokeSuspend execution:
public final void test(a) {
BuildersKt.launch$default((CoroutineScope)GlobalScope.INSTANCE, (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {
int label;
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
label26: {
Object var2;
label25: {
var2 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch(this.label) {
case 0:
ResultKt.throwOnFailure($result);
Log.d("test"."Network Request 1");
this.label = 1;
if (DelayKt.delay(1000L.this) == var2) {
return var2;
}
break;
case 1:
ResultKt.throwOnFailure($result);
break;
case 2:
ResultKt.throwOnFailure($result);
break label25;
case 3:
ResultKt.throwOnFailure($result);
break label26;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
Log.d("test"."Network Request 2");
this.label = 2;
if (DelayKt.delay(1000L.this) == var2) {
return var2;
}
}
Log.d("test"."Network Request 3");
this.label = 3;
if (DelayKt.delay(1000L.this) == var2) {
return var2;
}
}
Log.d("test"."end");
return Unit.INSTANCE;
}
/ /...
}), 3, (Object)null);
Copy the code
Does this code feel a little familiar, but you don’t understand it? So let’s go back a little bit, because we know from what we’ve seen before. Each suspended function creates a SuspendLambda. Does that mean that SuspendLambda objects need to be created for as many suspended functions as there are?
Now let’s assume that each suspended function creates a SuspendLambda. There are now three delays called in the test function, meaning at least three SuspendLambda objects in the test function. In the actual compiled Java code, however, we see only one SuspendLambda object. How does a coroutine maintain multiple suspended functions through a SuspendLambda object?
Now turn your attention back to the SuspendLambda function. There is only one label variable in the SuspendLambda function, and we don’t know what it does yet, but let’s keep it in mind.
The code in the invokeSuspend function looks a bit awkward and the logic is not particularly clear. Now I make a change to the invokeSuspend function, let’s see:
int label;
public final Object invokeSuspend(@NotNull Object $result) {
return label26($result)
}
public Object label26(Object $result){
return label25(var2);
}
public Object label25(Object $result,Object var2){
Object var2 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch(this.label) {
case 0:
ResultKt.throwOnFailure($result);
Log.d("test"."Network Request 1");
this.label = 1;
if (DelayKt.delay(1000L.this) == var2) {
return var2;
}
case 1:
ResultKt.throwOnFailure($result);
Log.d("test"."Network Request 2");
this.label = 2;
if (DelayKt.delay(1000L.this) == var2) {
return var2;
}
case 2:
ResultKt.throwOnFailure($result);
Log.d("test"."Network Request 3");
this.label = 3;
if (DelayKt.delay(1000L.this) == var2) {
return var2;
}
case 3:
ResultKt.throwOnFailure($result);
// break label26;
Log.d("test"."end");
return Unit.INSTANCE;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
}
internal fun Result<*>.throwOnFailure() {
if (value is Result.Failure) throw value.exception
}
Copy the code
Now look again like this is not clear at a glance, it is not too familiar. The corresponding suspend function is executed in invokeSuspend based on the value of the label. Now let’s break down the execution process:
-
Create a local variable var2 and assign COROUTINE_SUSPENDED to var2. Then execute switch through label to execute code in different states.
-
The first delay function is executed, because delay returns a COROUTINE_SUSPENDED for the first time, which is equal to var2. Returns the current invokeSuspend directly and returns COROUTINE_SUSPENDED to resumeWith, causing the coroutine to suspend.
-
When the coroutine resumes execution via resumeWith, the invokeSuspend function is called again. $result = $result; $result = $result; $result = $result; $result = $result; $result = $result; Similarly, because delay returns a COROUTINE_SUSPENDED for the first time, it will directly return the current invokeSuspend and return COROUTINE_SUSPENDED to resumeWith, causing the coroutine to suspend.
-
When the coroutine resumes with resumeWith completed by the second delay execution, the invokeSuspend function is called again. $result = $result; $result = $result; $result = $result; $result = $result; Similarly, because delay returns a COROUTINE_SUSPENDED for the first time, it will directly return the current invokeSuspend and return COROUTINE_SUSPENDED to resumeWith, causing the coroutine to suspend.
-
When the coroutine resumes resumeWith with the third delay, invokeSuspend is called again. The label status value is 3, and the Case 3 branch of SWtich will be executed. Since this is the end of the case, return the Unit object to the resumeWith function. Since the return value is not COROUTINE_SUSPENDED, a Result will be returned, and since Completion is no longer a SuspendLambda object that can continue execution, the final resumeWith returns the Result.
We can now determine that the coroutine fuses multiple suspended functions into a SuspendLambda object, maintaining a SuspendLambda execution step with a label value of type int. Controlled by label and switch statements, this is known as a state machine. No matter how many suspended functions there are after using the state machine, the compiler ends up creating only one SuspendLambda subclass and object.
Here’s a simple sequence diagram that you’ll see. I don’t need a bike if I can get the gist.
--)invokeSuspend: label = 0 invokeSuspend--)delay: label = 1 delay--)invokeSuspend: COROUTINE_SUSPENDED invokeSuspend--)resumeWith: COROUTINE_SUSPENDED delay--)resumeWith: resumeWith(result: Result<Any? > resumeWith--)invokeSuspend --)delay: label = 2 delay--)invokeSuspend: COROUTINE_SUSPENDED invokeSuspend--)resumeWith: COROUTINE_SUSPENDED delay--)resumeWith: resumeWith(result: Result<Any? >) resumeWith--)invokeSuspend --)delay: label = 3 delay--)invokeSuspend: COROUTINE_SUSPENDED invokeSuspend--)resumeWith: COROUTINE_SUSPENDED delay--)resumeWith: resumeWith(result: Result<Any? >) resumeWith--)invokeSuspend --)resumeWith: Result<Any? > resumeWith--) Result<Any? >
Look back here, a lot of content, a lot of code, look at so long estimates have been dizzy. Well, normally we’re at the end of this chapter. But with a sense of responsibility, keep going.
What difference does it make if we launch a child coroutine inside the parent coroutine and nest the execution:
public final void test(a) {
BuildersKt.launch$default((CoroutineScope)GlobalScope.INSTANCE, (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {
// $FF: synthetic field
private Object L$0;
int label;
@Nullable
public final Object invokeSuspend(@NotNull Object var1) {
Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch(this.label) {
case 0:
ResultKt.throwOnFailure(var1);
CoroutineScope $this$launch = (CoroutineScope)this.L$0;
BuildersKt.launch$default($this$launch, (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {
int label;
@Nullable
public final Object invokeSuspend(@NotNull Object var1) {
Object var2 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch(this.label) {
case 0:
ResultKt.throwOnFailure(var1);
return Unit.INSTANCE;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine"); }}@NotNull
public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
Intrinsics.checkNotNullParameter(completion, "completion");
Function2 var3 = new <anonymous constructor>(completion);
return var3;
}
public final Object invoke(Object var1, Object var2) {
return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE); }}),3, (Object)null);
return Unit.INSTANCE;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine"); }}@NotNull
public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
Intrinsics.checkNotNullParameter(completion, "completion");
Function2 var3 = new <anonymous constructor>(completion);
var3.L$0 = value;
return var3;
}
public final Object invoke(Object var1, Object var2) {
return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE); }}),3, (Object)null);
}
Copy the code
You can see that this is basically the same thing, except that a SuspendLambda object is created inside the subcoroutine and the same process is repeated. You can also see here that by default the parent and child coroutines have the same context. That’s why the child coroutine’s abnormal exit is passed on to the parent coroutine, causing the parent coroutine to also abnormally exit.
What if we were threading and switching through a withContext in a parent coroutine.
public final void test(a) {
BuildersKt.launch$default((CoroutineScope)GlobalScope.INSTANCE, (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {
int label;
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
Object var2 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch(this.label) {
case 0:
ResultKt.throwOnFailure($result);
CoroutineContext var10000 = (CoroutineContext)Dispatchers.getMain();
Function2 var10001 = (Function2)(new Function2((Continuation)null) {
int label;
/ /...
});
this.label = 1;
if (BuildersKt.withContext(var10000, var10001, this) == var2) {
return var2;
}
break;
case 1:
ResultKt.throwOnFailure($result);
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
return Unit.INSTANCE;
}
/ /...
}), 3, (Object)null);
}
Copy the code
The feel is similar to that of delay execution, except that withContext adds a SuspendLambda object called Var10001. Internal logic is much the same, here is no longer expanded, interested can take a look at the source code.
So that’s the end of the chapter. I’m a lazy person, and I wrote this series out of sheer boredom. As for the next update, that depends on fate. Maybe tomorrow, maybe a week, maybe even a month……
trailer
In the next article we will explain the principle of coroutine scheduler and how coroutines are scheduled.
Originality is not easy. If you like this article, you can move your little hands to like collection, your encouragement will be transformed into my motivation to move forward.
- Basic usage of Kotlin coroutines
- Kotlin coroutine introduction to Android version in detail (ii) -> Kotlin coroutine key knowledge points preliminary explanation
- Kotlin coroutine exception handling
- Use Kotlin coroutine to develop Android applications
- Network request encapsulation for Kotlin coroutines
- Kotlin coroutine theory (1)
- Kotlin coroutine theory (2)
Extend the series
- Encapsulating DataBinding saves you thousands of lines of code
- A ViewModel wrapper for everyday use