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
If you’ve been around coroutines, you’ve probably had the following questions:
- What the hell is a coroutine?
- coroutines
suspend
What does it do? How does it work? - Some key names in coroutines (e.g.
Job
,Coroutine
,Dispatcher
,CoroutineContext
withCoroutineScope
) What is the relationship between them? - What is the so-called non-blocking suspend and resume of coroutines?
- What is the internal implementation of coroutines?
- .
The next few articles will try to analyze these questions, and you are welcome to join the discussion.
CoroutineScope
What is a CoroutineScope? If that sounds unfamiliar to you, GlobalScope, lifecycleScope, and viewModelScope are (for Android developers, of course). They all implement the CoroutineScope interface.
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
}
Copy the code
CoroutineScope contains only one variable to be implemented, CoroutineContext. As for CoroutineContext, the internal structure of CoroutineContext has been analyzed in previous articles, so it is no longer cumbersome.
By its structure, we can think of it as a container that provides CoroutineContext, ensures that CoroutineContext can be passed through the entire coroutine operation, and constrains the boundary of CoroutineContext.
For example, if you use a coroutine in Android to request data, the Activity exits before the interface completes the request, and not stopping the running coroutine can have unexpected consequences. So we recommend using lifecycleScope to start coroutines in all activities. LifecycleScope gives coroutines the same lifecycle awareness as activities.
LifecycleScope source code:
val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope get() = lifecycle.coroutineScope val Lifecycle.coroutineScope: LifecycleCoroutineScope get() { while (true) { val existing = mInternalScopeRef.get() as LifecycleCoroutineScopeImpl? if (existing ! = null) { return existing } val newScope = LifecycleCoroutineScopeImpl( this, SupervisorJob() + Dispatchers.Main.immediate ) if (mInternalScopeRef.compareAndSet(null, newScope)) { newScope.register() return newScope } } }Copy the code
It creates a LifecycleCoroutineScopeImpl instance, it implements the CoroutineScope interface, at the same time the incoming SupervisorJob () + Dispatchers. The Main CoroutineContext as it.
Let’s look at its register() method again
internal class LifecycleCoroutineScopeImpl( override val lifecycle: Lifecycle, override val coroutineContext: CoroutineContext ) : LifecycleCoroutineScope(), LifecycleEventObserver { init { // in case we are initialized on a non-main thread, make a best effort check before // we return the scope. This is not sync but if developer is launching on a non-main // dispatcher, they cannot be 100% sure anyways. if (lifecycle.currentState == Lifecycle.State.DESTROYED) { coroutineContext.cancel() } } fun register() { // TODO use Main.Immediate once it is graduated out of experimental. launch(Dispatchers.Main) { if (lifecycle.currentState >= Lifecycle.State.INITIALIZED) { lifecycle.addObserver(this@LifecycleCoroutineScopeImpl) } else { coroutineContext.cancel() } } } override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { if (lifecycle.currentState <= Lifecycle.State.DESTROYED) { lifecycle.removeObserver(this) coroutineContext.cancel() } }Copy the code
A coroutine is created using the classic launch in the register method, and the CoroutineContext used by launch is the CoroutineContext in CoroutineSope. The Lifecycle feature of Jetpack is then combined with the coroutine to listen for the Lifecycle of the Activiyt.
If you are not familiar with the use and features of Lifecycle, I recommend reading this introductory article Android Architecture Components Part3:Lifecycle
This means that the following method is called to cancel the coroutine when the Activity is destroyed.
coroutineContext.cancel()
Copy the code
This is where CoroutineContext is used. After the analysis of the previous article, it is easy to know that CoroutineContext itself does not have a cancel method, so this cancel method is an extension of CoroutineContext.
public fun CoroutineContext.cancel(): Unit { this[Job]? .cancel() }Copy the code
So the real logic from CoroutineContex set to retrieve the Key Job for instance, the corresponding is created above LifecycleCoroutineScopeImpl instances when the incoming SupervisorJob, It is one of the subclasses of CoroutineContext.
Now let’s look at some of the lifecycleScope related methods
lifecycleScope.launchWhenCreated { }
lifecycleScope.launchWhenStarted { }
lifecycleScope.launchWhenResumed { }
Copy the code
The internal logic of these methods is obvious, namely that Lifecycle is used to trace the Activity’s Lifecycle, thereby constraining the timing of the coroutine to run.
We can also implement a CoroutineScope without using lifecycleScope and have it do the same thing in an Activity.
class MyActivity : AppCompatActivity(), CoroutineScope {
lateinit var job: Job
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
job = Job()
}
override fun onDestroy() {
super.onDestroy()
job.cancel() // Cancel job on activity destroy. After destroy all children jobs will be cancelled automatically
}
/*
* Note how coroutine builders are scoped: if activity is destroyed or any of the launched coroutines
* in this method throws an exception, then all nested coroutines are cancelled.
*/
fun loadDataFromUI() = launch { // <- extension on current activity, launched in the main thread
val ioData = async(Dispatchers.IO) { // <- extension on launch scope, launched in IO dispatcher
// blocking I/O operation
}
// do something else concurrently with I/O
val data = ioData.await() // wait for result of I/O
draw(data) // can draw in the main thread
}
}
Copy the code
The above implementation also ensures that the coroutine in the current Activiyt terminates when the Activity is destroyed.
Here the role of CoroutineScope is clear, it is used to constrain the boundary of coroutines, can provide the corresponding coroutine cancellation function, ensure the operation range of coroutines.
This, of course, leads to another topic
What is Job?
Job
Basically every time you start a coroutine you get a corresponding Job, for example
lifecycleScope.launch {
}
Copy the code
Launch returns a Job that can be used to manage coroutines. Multiple sub-jobs can be associated with a Job, and it also provides the implementation of passing parent from outside
public fun Job(parent: Job? = null): Job = JobImpl(parent)
Copy the code
When passed to a parent, the Job will be a child Job of the parent.
Since a Job manages coroutines, it provides six states to represent the running state of the coroutine.
New
: createActive
Run:Completing
: The subcoroutine has finished waiting for itselfCompleted
Completed:Cancelling
: Canceling or failingCancelled
: Cancels or fails
The six states of jobs expose three states, which can be obtained through jobs at any time
public val isActive: Boolean
public val isCompleted: Boolean
public val isCancelled: Boolean
Copy the code
So if you need to manually manage coroutines yourself, you can check to see if the coroutine is running.
While (job.isactive) {// Coroutine running}Copy the code
In general, coroutines are created in the Active state, but there are exceptions.
For example, we can pass a start parameter when launching a coroutine via launch
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
...
}
Copy the code
If the start passes CoroutineStart.LAZY, it will be in the New state. The coroutine can be invoked to enter the Active state by calling start or JOIN.
Let’s take a look at a simple diagram to understand the transition process between the six states of Job.
wait children
+-----+ start +--------+ complete +-------------+ finish +-----------+
| New | -----> | Active | ---------> | Completing | -------> | Completed |
+-----+ +--------+ +-------------+ +-----------+
| cancel / fail |
| +----------------+
| |
V V
+------------+ finish +-----------+
| Cancelling | --------------------------------> | Cancelled |
+------------+ +-----------+
Copy the code
As mentioned above, a Job can have multiple child jobs. Therefore, the completion of a Job must wait for all its child jobs to complete. The corresponding cancel is the same.
By default, if an internal child Job is abnormal, its parent Job and its related child jobs are cancelled. Commonly known as a chain reaction.
We can also change the default, and Kotlin provides the SupervisorJob to change it. It’s also common to see a situation where you’re using a coroutine to request two interfaces, but you don’t want one interface to fail and the other interface won’t be able to request it, so it’s useful to SupervisorJob to change the default mechanism for the coroutine.
It’s easy to use, and when we’re creating the CoroutineContext we’re going to add the container job. The lifecycleScope mentioned above, for example, is designed to be handled inside the container with a small container called the SupervisorJob
val newScope = LifecycleCoroutineScopeImpl(
this,
SupervisorJob() + Dispatchers.Main
)
Copy the code
You can also run the following example and replace it with another CoroutineContext in the container to see how it looks.
fun main() = runBlocking { val supervisor = SupervisorJob() with(CoroutineScope(coroutineContext + supervisor)) { // Start the first child job -- this example will ignore its exception (don't do this in practice!). val firstChild = launch(CoroutineExceptionHandler { _, _ ->}) {println("The first child is failing") throw AssertionError("The first child is cancelled") SecondChild = launch {firstchild.join () // Println ("The first child is cancelled: ${firstChild.isCancelled}, But the second one is still active") try {delay(long.max_value)} finally {// But unsupervised propagation println(" the second child is Cancelled because the supervisor was cancelled")}} firstChild.join() println("Cancelling the supervisor") supervisor.cancel() secondChild.join() } }Copy the code
If there are tasks that you don’t want to manually cancel, use NonCancellable as the CoroutineContext for the task
If you need a Job to retrieve the result of the coroutine, you can use Deferred, which is a subclass of Job and has the same functionality as Job. It also provides an additional await method to wait for the coroutine result to return.
Deferred can be created with coroutinescope.async.
Start and cancel Job methods start and cancel Job methods
invokeOnCompletion
This method is a callback notification for the Job and is called when the Job is finished executing
public fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle
public typealias CompletionHandler = (cause: Throwable?) -> Unit
Copy the code
There are three cases of this cause:
is null
: The coroutine is successfully executedis CancellationException
: The coroutine cancels normally, not due to an exceptionOtherwise
: The coroutine is abnormal
At the same time, its return value DisposableHandle can be used to disable the listener for the callback.
join
public suspend fun join()
Copy the code
Note that this is a suspend function, so it can only be called from suspend or coroutine.
It suspends the current coroutine task and executes the coroutine task of its own Job immediately. It does not resume the previous coroutine task until its execution is complete.
This article mainly introduces the function of CoroutineScope and the state evolution and application of Job. 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.