An article takes you to thoroughly understand Kotlin's coroutines

Background

In order to solve the callback hell generated by asynchronous threads

//传统回调方式
api.login(phone,psd).enquene(new Callback<User>(){
  public void onSuccess(User user){
    api.submitAddress(address).enquene(new Callback<Result>(){
      public void onSuccess(Result result){
        ...
      }
    });
  }
});
//使用协程后
val user=api.login(phone,psd)
api.submitAddress(address)
...

What is a coroutine

Essentially, coroutines are lightweight threads.

Key nouns for coroutines

val job = GlobalScope.launch {
    delay(1000)
    println("World World!")
}
  • CoroutineScope (Scope of Action)

    Control the execution thread and life cycle of the coroutine code block, including GlobeScope, lifecycleScope, viewModelScope and other custom CoroutineScope

    GlobeScope: global scope, will not automatically end execution

    lifecycleScope: lifecycle scope, used for activity and other lifecycle components, it will automatically end when DESTROYED, and additional introduction is required

    viewModelScope: viewModel scope, used in ViewModel, will automatically end when ViewModel is recycled, additional introduction is required

  • Job

    The unit of measurement of the coroutine, which is equivalent to a job task. The launch method returns a new Job by default

  • suspend

    Acting on the method, it means that the method is a time-consuming task, such as the delay method above

public suspend fun delay(timeMillis: Long) {
    ...
}

The introduction of coroutines

Main frame ($coroutines_version is replaced with the latest version, such as 1.3.9, the same below)

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"

lifecycleScope (optional, version 2.2.0)

implementation 'androidx.activity:activity-ktx:$lifecycle_scope_version'

viewModelScope (optional, version 2.3.0-beta01)

implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$coroutines_viewmodel_version"

Simple to use

Let me give you a simple example

lifecycleScope.launch { 
    delay(2000)
    tvTest.text="Test"
}

The function implemented in the above example is to wait for 2 seconds, and then modify the text value of the TextView control whose id is tvTest to Test

Custom delay return method

In Kotlin, for methods that require delay to return results, you need to use suspend to indicate

lifecycleScope.launch {
    val text=getText()
    tvTest.text = text
}
suspend fun getText():String{
    delay(2000)
    return "getText"
}

If you need to use Continuation for thread switching in other threads, you can use suspendCancellableCoroutine or suspendCoroutine to wrap (the former can be cancelled, which is equivalent to the extension of the latter), call it.resume() successfully, call it.resumeWithException(Exception()) if it fails, Throw an exception

suspend fun getTextInOtherThread() = suspendCancellableCoroutine<String> {
    thread {
        Thread.sleep(2000)
        it.resume("getText")
    }
}

Exception catch

Failures in the coroutine can be caught by exceptions to deal with special situations uniformly

lifecycleScope.launch {
    try {
        val text=getText()
        tvTest.text = text
    } catch (e:Exception){
        e.printStackTrace()
    }
}

Cancel function

Two jobs are executed below, the first one is original, the second one is to cancel the first job after 1 second, which will cause the text of tvText to not change

val job = lifecycleScope.launch {
    try {
        val text=getText()
        tvTest.text = text
    } catch (e:Exception){
        e.printStackTrace()
    }
}
lifecycleScope.launch {
    delay(1000)
    job.cancel()
}

Set timeout

This is equivalent to the system encapsulating the automatic cancellation function, corresponding to the function withTimeout

lifecycleScope.launch {
    try {
        withTimeout(1000) {
            val text = getText()
            tvTest.text = text
        }
    } catch (e:Exception){
        e.printStackTrace()
    }
}

Job with return value

Similar to launch, there is also an async method, which returns a Deferred object, which belongs to the extended class of Job. Deferred can get the returned result. The specific usage is as follows

lifecycleScope.launch {
    val one= async {
        delay(1000)
        return@async 1
    }
    val two= async {
        delay(2000)
        return@async 2
    }
    Log.i("scope test",(one.await()+two.await()).toString())
}

Advanced advanced

Custom CoroutineScope

First look at the CoroutineScope source code

public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}

CoroutineScope mainly contains a coroutineContext object, we only need to implement the get method of coroutineContext to customize

class TestScope() : CoroutineScope {
    override val coroutineContext: CoroutineContext
        get() = TODO("Not yet implemented")
}

To create a coroutineContext, you must first know what CoroutineContext is, and then we look at the source code of CoroutineContext

/**
 * Persistent context for the coroutine. It is an indexed set of [Element] instances.
 * An indexed set is a mix between a set and a map.
 * Every element in this set has a unique [Key].
 */
public interface CoroutineContext {
    public operator fun <E : Element> get(key: Key<E>): E?
    public fun <R> fold(initial: R, operation: (R, Element) -> R): R
    public operator fun plus(context: CoroutineContext): CoroutineContext = 
        ...
    public fun minusKey(key: Key<*>): CoroutineContext

    public interface Key<E : Element>
    public interface Element : CoroutineContext {
        ...
    }
}

Through comments, we know that it is essentially a collection containing Element, but unlike the set and map collections, it implements acquisition (get), folding (fold, a combination of addition and replacement), and subtraction (minusKey, shift). Except), object combination (plus, such as val coroutineContext=coroutineContext1+coroutineContext2)

Its main content is Element, and the realization of Element has

  • Job task
  • ContinuationInterceptor interceptor
  • AbstractCoroutineContextElement
  • CoroutineExceptionHandler
  • ThreadContextElement
  • DownstreamExceptionElement

You can see that Element is implemented in many places, and its main purpose is to limit the scope and handle exceptions. Here we first understand two important elements, one is Job and the other is CoroutineDispatcher

Job
  • Job: If the child job is cancelled, the parent job and other child jobs will be cancelled; if the parent job is cancelled, all child jobs will be cancelled
  • SupervisorJob: the parent job is cancelled, all child jobs are cancelled
CoroutineDispatcher
  • Dispatchers.Main: main thread execution
  • Dispatchers.IO: IO thread execution

We simulate a custom TestScope similar to lifecycleScope

class TestScope() : CoroutineScope {
    override val coroutineContext: CoroutineContext
        get() = SupervisorJob() +Dispatchers.Main
}

Here we define a total process line SupervisorJob() and a specific execution environment Dispatchers.Main (Android main thread). If we want to replace the lifecycleScope of the activity, we need to create an instance in the activity

val testScope=TestScope()

Then cancel all jobs when the activity is destroyed

override fun onDestroy() {
    testScope.cancel()
    super.onDestroy()
}

Other usage methods are the same as lifecycleScope, such as

testScope.launch{
    val text = getText()
    tvTest.text = text
}

Deep understanding of Job

CoroutineScope contains a main job, and the jobs created by launch or other methods that are called later belong to the child jobs of CoroutineScope. Each job has its own state, including isActive, isCompleted, isCancelled, and some basic operations start(), cancel(), join(), the specific conversion process is as follows

job status diagram.png

Let's start with creating a job. When launch is called, there are three parameters CoroutineContext, CoroutineStart, and code block parameters by default.

  • context: The object of CoroutineContext, the default is CoroutineStart.DEFAULT, which will be folded with the context of CoroutineScope
  • start: The object of CoroutineStart, the default is CoroutineStart.DEFAULT, which represents immediate execution, and CoroutineStart.LAZY, which represents non-immediate execution, must call start() of the job to start execution
val job2= lifecycleScope.launch(start =  CoroutineStart.LAZY) {
    delay(2000)
    Log.i("scope test","lazy")
}
job2.start()

When created in this mode, the default is the new state. At this time, isActive, isCompleted, and isCancelled are all false. When start is called, it is converted to the active state. Only isActive is true. If its task is completed, it will enter the Completing state. At this time, it is waiting for the completion of the child job. In this state, only isActive is true. If all child jobs are also completed, it will enter the Completed state, and only isCompleted is true. If there is a cancellation or exception in the active or Completing state, it will enter the Cancelling state. If it is necessary to cancel the parent job and other child jobs, it will wait for their cancellation to complete. At this time, only isCancelled is true. After the cancellation is completed, it will finally enter the Cancelled state, isCancelled And isCompleted are both true

State isActive isCompleted isCancelled
New FALSE FALSE FALSE
Active TRUE FALSE FALSE
Completing TRUE FALSE FALSE
Cancelling FALSE FALSE TRUE
Cancelled FALSE TRUE TRUE
Completed FALSE TRUE FALSE

Different job interactions need to use join() and cancelAndJoin()

  • join(): Add the current job to other coroutine tasks
  • cancelAndJoin(): cancel the operation, just add it and then cancel it
val job1= GlobleScope.launch(start =  CoroutineStart.LAZY) {
    delay(2000)
    Log.i("scope test","job1")
}
lifecycleScope.launch {
    job1.join()
    delay(2000)
    Log.i("scope test","job2")
}

Deep understanding of suspend

Suspend as a new method modifier of kotlin, the final implementation is still java, let's look at their differences

suspend fun test1(){}
fun test2(){}

Corresponding to java code

public final Object test1(@NotNull Continuation $completion) {
  return Unit.INSTANCE;
}
public final void test2() {
}

Corresponding bytecode

public final test1(Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
  ...
   L0
    LINENUMBER 6 L0
    GETSTATIC kotlin/Unit.INSTANCE : Lkotlin/Unit;
    ARETURN
   L1
    LOCALVARIABLE this Lcom/lieni/android_c/ui/test/TestActivity; L0 L1 0
    LOCALVARIABLE $completion Lkotlin/coroutines/Continuation; L0 L1 1
    MAXSTACK = 1
    MAXLOCALS = 2

public final test2()V
   L0
    LINENUMBER 9 L0
    RETURN
   L1
    LOCALVARIABLE this Lcom/lieni/android_c/ui/test/TestActivity; L0 L1 0
    MAXSTACK = 0
    MAXLOCALS = 1

As you can see, the method with suspend is actually the same as the normal method, except that there is an additional Continuation object when it is passed in, and the Unit.INSTANCE object is returned.

Continuation is an interface that contains the context object and resumeWith method

public interface Continuation<in T> {
    public val context: CoroutineContext
    public fun resumeWith(result: Result<T>)
}

The specific implementation of Continuation is in BaseContinuationImpl

internal abstract class BaseContinuationImpl(...) : Continuation<Any?>, CoroutineStackFrame, Serializable {
    public final override fun resumeWith(result: Result<Any?>) {
        ...
        while (true) {
            ...
            with(current) {
              	val outcome = invokeSuspend(param)
                ...
                releaseIntercepted() 
                if (completion is BaseContinuationImpl) {
                    ...
                } else {
                    ...
                    return
                }
            }
        }
    }
    ...
}

When we call resumeWith, it will continue to execute a loop, call invokeSuspend(param) and releaseIntercepted(), until the top-level completion is completed and return, and release the interceptor of the coroutine

The final release is implemented in ContinuationImpl

internal abstract class ContinuationImpl(...) : BaseContinuationImpl(completion) {
    ...
    protected override fun releaseIntercepted() {
        val intercepted = intercepted
        if (intercepted != null && intercepted !== this) {
            context[ContinuationInterceptor]!!.releaseInterceptedContinuation(intercepted)
        }
        this.intercepted = CompletedContinuation 
    }
}

Through here, the release is finally realized through the Element of ContinuationInterceptor in CoroutineContext

The same is true for suspending, continue to look at suspendCoroutine

public suspend inline fun <T> suspendCoroutine(crossinline block: (Continuation<T>) -> Unit): T =
    suspendCoroutineUninterceptedOrReturn { c: Continuation<T> ->
        val safe = SafeContinuation(c.intercepted())
        ...
    }

The intercepted() method of Continuation is called by default

internal abstract class ContinuationImpl(...) : BaseContinuationImpl(completion) {
    ...
    public fun intercepted(): Continuation<Any?> =intercepted
            ?: (context[ContinuationInterceptor]?.interceptContinuation(this) ?: this)
                .also { intercepted = it }
}

It can be seen that the suspension is finally realized by the Element which is the ContinuationInterceptor in the CoroutineContext.

Process summary (thread switching)

  • Create a new Continuation
  • Call the interceptContinuation method of the context's ContinuationInterceptor in CoroutineScope to suspend the parent task
  • Execute subtasks (if a thread is specified, it will be executed in a new thread and the Continuation object will be passed in)
  • After the execution is completed, the user calls resume or resumeWith of Continuation to return the result
  • Call the releaseInterceptedContinuation method of the context's ContinuationInterceptor in CoroutineScope to restore the parent task

Blocking and non-blocking

CoroutineScope does not block the current thread by default. If you need to block, you can use runBlocking. If you execute the following code in the main thread, a 2s white screen will appear.

runBlocking { 
    delay(2000)
    Log.i("scope test","runBlocking is completed")
}

Blocking principle: Executing runBlocking will create BlockingCoroutine by default, and BlockingCoroutine will continue to execute a loop until the current Job is in the isCompleted state.

public fun <T> runBlocking(...): T {
    ...
    val coroutine = BlockingCoroutine<T>(newContext, currentThread, eventLoop)
    coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
    return coroutine.joinBlocking()
}
private class BlockingCoroutine<T>(...) : AbstractCoroutine<T>(parentContext, true) {
    ...
    fun joinBlocking(): T {
      ...
      while (true) {
        ...
        if (isCompleted) break
        ...
      }    
      ...
    }
}

Let me share with you a copy of Google's advanced Kotlin enhanced practical manual (with Demo).

Chapter 1 Introduction to Kotlin

  • Kotlin overview
  • Comparison of Kotlin and Java
  • Using Android Studio skillfully
  • Know the basic types of Kotlin
  • Walk into Kotlin's array
  • Walk into the Kotlin collection
  • Collection problem
  • Complete code
  • Basic grammar

Chapter 2 Kotlin Actual Combat Avoiding Pit Guide

  • Method input parameters are constants and cannot be modified
  • No Companion, INSTANCE?
  • Java overloading, how do you make a clever transition in Kotlin?
  • Empty posture in Kotlin
  • Kotlin overwrites the methods in the Java parent class
  • Kotlin gets "ruthless" and even TODO doesn't let it go!
  • pit in is, as`
  • Understanding of Property in Kotlin
  • also keyword
  • takeIf keyword
  • takeIf keyword
  • Singleton mode

Chapter 3 Project Actual Combat "Kotlin Jetpack Actual Combat"

  • Starting from a demo that worships a great god
  • What kind of experience is Kotlin writing Gradle scripts?
  • The triple realm of Kotlin programming
  • Kotlin higher-order functions
  • Kotlin generics
  • Kotlin extension
  • Kotlin commission
  • "Unknown" debugging skills for coroutines
  • Graphical coroutine: suspend

Friends who need this PDF document can join the communication skirt here, front: 1102, middle: 405, and finally: 044. There are students and big guys in the skirt, and the resources are free to share.

Guess you like

Origin blog.csdn.net/zhireshini233/article/details/114854185