Today we’re going to talk about Kotlin’s Coroutine.

If you haven’t been exposed to coroutines, I recommend reading this introductory article What? You don’t know Kotlin Coroutine?

If you have been exposed to coroutines and are confused about the principles of coroutines, it is recommended that you read the following articles before reading this article so that you can understand it more thoroughly and smoothly.

Kotlin coroutine implementation principle :Suspend&CoroutineContext

Kotlin coroutine implementation principle :Coroutine type &Job

Kotlin coroutine implementation principle :ContinuationInterceptor&CoroutineDispatcher

If you’ve been around coroutines, you’ve probably had the following questions:

  1. What the hell is a coroutine?
  2. coroutinessuspendWhat does it do? How does it work?
  3. Some key names in coroutines (e.g.Job,Coroutine,Dispatcher,CoroutineContextwithCoroutineScope) What is the relationship between them?
  4. What is the so-called non-blocking suspend and resume of coroutines?
  5. What is the internal implementation of coroutines?
  6. .

The next few articles will try to analyze these questions, and you are welcome to join the discussion.

hang

A coroutine is guaranteed to run using a non-blocking suspend. So what is a non-blocking suspend? Now let’s talk about what a hang is.

The suspend keyword was mentioned in the previous article. One of its functions is to add a continuation-type argument to a method when the code is called, allowing Continuaton to pass up and down the coroutine.

Its other key role is to play a suspended coroutine identification.

It is possible to suspend the current coroutine every time it encounters a method decorated by suspend.

Note that it is possible.

You can write any method that is also decorated by suspend, but it is not suspended when called from a coroutine. For example,

private suspend fun a() {
	println("aa")
}

lifecycleScope.launch {
	a()
}
Copy the code

This method does not return the COROUTINE_SUSPENDED type.

A suspended coroutine returns the COROUTINE_SUSPENDED flag in the corresponding state.

A little deeper involves state machines. Inside the coroutine is the use of a state machine to manage the various hang points of the coroutine.

The text is a little abstract, so let’s look at the code. Let’s take the example of method A above.

Start by opening Kotlin Bytecode for this code in Android Studio. To open it, go to Tools -> Kotlin -> Show Kotlin Bytecode.

Then click on the Decompile option to generate the decompiled Java code. The final code is as follows:

BuildersKt.launch$default((CoroutineScope)LifecycleOwnerKt.getLifecycleScope(this), (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) { private CoroutineScope p$; Object L$0; int label; @nullable public final Object invokeSuspend(@notnull Object $result) IntrinsicsKt.getCOROUTINE_SUSPENDED(); CoroutineScope $this$launch; switch(this.label) { case 0: ResultKt.throwOnFailure($result); $this$launch = this.p$; MainActivity var10000 = MainActivity.this; $this.L$0 = $this$launch; This.label = 1; this.label = 1; If (var10000. A (this) == var3) {return var3; } // No need to suspend, the coroutine continues to execute other logic breaks; Case 1: $this$launch = (CoroutineScope) this.l $0; ResultKt. ThrowOnFailure ($result); break; default: throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine"); } return Unit.INSTANCE; } @NotNull public final Continuation create(@Nullable Object value, @NotNull Continuation completion) { Intrinsics.checkParameterIsNotNull(completion, "completion"); Function2 var3 = new <anonymous constructor>(completion); var3.p$ = (CoroutineScope)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

The code above is the state machine of the coroutine, representing different states by label, thus executing the logical code in different cases.

As described in the previous article, the coroutine starts with a manual call to the resumeWith method, whose internal logic is to execute the invokeSuspend method above.

So the label value is 0 when the coroutine is first run and case 0: is entered. At this point, the site is recorded to prepare for the possible pending state and set the next possible state to be executed.

If method A returns var3, the var3 corresponds to COROUTINE_SUSPENDED. Therefore, only when a method returns COROUTINE_SUSPENDED will the if internal statement be executed to exit the method, at which point the coroutine is suspended. The current thread can execute other logic without being blocked by the suspension of the coroutine.

So the suspension of a coroutine at the code level is to jump out of the method body of the coroutine execution, or jump out of the corresponding state in the current state machine of the coroutine, and then wait for the next state to execute.

So why doesn’t this method a that we wrote get suspended?

@Nullable
final Object a(@NotNull Continuation $completion) {
   return Unit.INSTANCE;
}
Copy the code

Its return value is not COROUTINE_SUSPENDED.

Since it will not be suspended, under what circumstances will a method be suspended?

Very simply, if we add the delay method to method A, it will be suspended.

@Nullable
final Object a(@NotNull Continuation $completion) {
   Object var10000 = DelayKt.delay(1000L, $completion);
   return var10000 == IntrinsicsKt.getCOROUTINE_SUSPENDED() ? var10000 : Unit.INSTANCE;
}
Copy the code

It is the Delay method that really triggers the suspension, because the Delay method creates its own Continuation and internally calls the getResult method.

 internal fun getResult(): Any? {
     installParentCancellationHandler()
     if (trySuspend()) return COROUTINE_SUSPENDED
     // otherwise, onCompletionInternal was already invoked & invoked tryResume, and the result is in the state
     val state = this.state
     if (state is CompletedExceptionally) throw recoverStackTrace(state.cause, this)
     return getSuccessfulResult(state)
 }
Copy the code

The current coroutine is suspended by trySuspend in the getResult method. Triggers the suspension of the parent class’s coroutine by suspending its own coroutine.

For testing purposes, you can have method A return COROUTINE_SUSPENDED directly

 private suspend fun a(): Any {
     return kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED
 }
Copy the code

Of course lines should never be written like this, because once written like this the coroutine will always be suspended because you have no ability to recover it.

restore

Now let’s talk a little bit more about coroutine recovery.

The recovery nature of coroutines is triggered by the resumeWith method of continuations.

Let’s look at a suspending example to analyze the entire process of coroutine suspension and recovery.

println("main start")
lifecycleScope.launch {
   println("async start")
   val b = async {
       delay(2000)
       "async"
   }
   b.await()
   println("async end")
}
Handler().postDelayed({
   println("main end")
}, 1000)
Copy the code

The Kotlin code is very simple. While the coroutine is running with the main thread, an async method is executed internally and the suspension of the coroutine is triggered with an await method.

Take a look at its decompiled Java counterpart

// 1 String var2 = "main start"; System.out.println(var2); BuildersKt.launch$default((CoroutineScope)LifecycleOwnerKt.getLifecycleScope(this), (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) { private CoroutineScope p$; Object L$0; Object L$1; int label; @Nullable public final Object invokeSuspend(@NotNull Object $result) { Object var5 = IntrinsicsKt.getCOROUTINE_SUSPENDED(); CoroutineScope $this$launch; Deferred b; switch(this.label) { case 0: // 2 ResultKt.throwOnFailure($result); $this$launch = this.p$; String var6 = "async start"; System.out.println(var6); b = BuildersKt.async$default($this$launch, (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) { private CoroutineScope p$; Object L$0; int label; @Nullable public final Object invokeSuspend(@NotNull Object $result) { Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED(); CoroutineScope $this$async; switch(this.label) { case 0: // 3 ResultKt.throwOnFailure($result); $this$async = this.p$; this.L$0 = $this$async; this.label = 1; if (DelayKt.delay(2000L, this) == var3) { return var3; } break; Case 1: // 5, 6 $this$async = (CoroutineScope)this. ResultKt.throwOnFailure($result); break; default: throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine"); } return "async"; } @NotNull public final Continuation create(@Nullable Object value, @NotNull Continuation completion) { Intrinsics.checkParameterIsNotNull(completion, "completion"); Function2 var3 = new <anonymous constructor>(completion); var3.p$ = (CoroutineScope)value; return var3; } public final Object invoke(Object var1, Object var2) { return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE); } }), 3, (Object)null); this.L$0 = $this$launch; this.L$1 = b; this.label = 1; if (b.await(this) == var5) { return var5; } break; case 1: // 7 b = (Deferred)this.L$1; $this$launch = (CoroutineScope)this.L$0; ResultKt.throwOnFailure($result); break; default: throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine"); } // 8 String var4 = "async end"; System.out.println(var4); return Unit.INSTANCE; } @NotNull public final Continuation create(@Nullable Object value, @NotNull Continuation completion) { Intrinsics.checkParameterIsNotNull(completion, "completion"); Function2 var3 = new <anonymous constructor>(completion); var3.p$ = (CoroutineScope)value; return var3; } public final Object invoke(Object var1, Object var2) { return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE); } }), 3, (Object)null); // 4 (new Handler()).postDelayed((Runnable)null.INSTANCE, 1000L);Copy the code

It’s a little long, it doesn’t matter but let’s just look at the key points, look at the state machine.

  1. First of all beThe outputmain startAnd then throughlaunchCreate coroutine, enter coroutine state machine, at this pointlabelfor0, the implementation ofcase: 0Correlation logic.
  2. Enter thecase: 0afterThe outputasync start, the callasyncAnd through theawaitTo suspend the current coroutine, record the data of the current point of suspension during resuspend, and willlableSet to1.
  3. Enter theasyncCreate the coroutine at this timeasyncCoroutines inlablefor0And into theasync case: 0performdealyAnd hangasyncCoroutines. And will belabelSet to1. Waiting for the2sAnd then they wake up.
  4. All coroutines are suspended, that is, coroutines are jumpedlaunchMethod, executionhandlerOperation. Due to thepost 1sSo compared to coroutinesdealyIt’s short, so it’s a priorityThe outputmain endAnd then over1sTo enter the recovery coroutine phase
  5. asyncThe coroutine indelayRecovery, pay attention todelayMethod is passed inthis.asynctheContinuationObject, sodelayOnce the interior is complete2sThe timer will be calledContinuationtheresumeWithMethods to recoverasyncThe coroutine, that is, the callinvokeSuspendMethods.
  6. Due to being suspended before already willasync labelSet to1, so entercase: 1, resuming the site before suspending, checking for anomalies, and finally returningasync.
  7. At this timeawaitThe hang point is restored, notice that it is also passed inthisTheta corresponds to thetalaunchIn theContinuation, so there will be a callbackresumeWithMethod, the final callinvokeSuspend, that is, to entercase 1:Restore the scene and end the state machine.
  8. Finally, async end is output, and the coroutine is finished.

We can execute the above code to verify that the output is correct

main start
async start
main end
async end
Copy the code

To summarize, the point of suspension of coroutines is identified by suspend, but the real point of suspension is also determined by whether to return COROUTINE_SUSPENDED, while code embodiment handles the suspension and recovery of coroutines through the state machine. When it is necessary to suspend, leave the scene and set the next state point, and then suspend the coroutine by exiting the method. The current thread is not blocked during suspension. The corresponding recovery is performed by resumeWith to enter the next state of the state machine, while resuming the previously suspended scene when entering the next state.

This paper mainly introduces the principle of coroutine suspension and recovery, and also analyzes the execution process of coroutine state machine. Hope to learn coroutine partners can be helpful, please look forward to the subsequent coroutine analysis.

project

Android_startup: provides a simple and efficient way to initialize components during application startup, optimizing startup speed. Not only does it support all the features of Jetpack App Startup, but it also provides additional synchronous and asynchronous waiting, thread control, and multi-process support.

AwesomeGithub: Based on Github client, pure exercise project, support componentized development, support account password and authentication login. Kotlin language for development, the project architecture is based on Jetpack&DataBinding MVVM; Popular open source technologies such as Arouter, Retrofit, Coroutine, Glide, Dagger and Hilt are used in the project.

Flutter_github: a cross-platform Github client based on Flutter, corresponding to AwesomeGithub.

Android-api-analysis: A comprehensive analysis of Knowledge points related to Android with detailed Demo to help readers quickly grasp and understand the main points explained.

Daily_algorithm: an algorithm of the day, from shallow to deep, welcome to join us.