background

To solve the callback hell created by asynchronous threads

// Traditional callback
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){... }}); }});Copy the code
// After using coroutines
val user=api.login(phone,psd)
api.submitAddress(address)
...
Copy the code

What is a coroutine

In essence, coroutines are lightweight threads.

Coroutine key nouns

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

    Controls the threads, lifecycleScope, viewModelScope, and other custom CoroutineScope blocks for execution

    GlobeScope: global scope that does not automatically end execution

    LifecycleScope: Lifecycle range used for activities and other life-cycle components that are automatically DESTROYED and need to be added

    ViewModelScope: The scope of the viewModel, used in the viewModel, which is automatically terminated when the viewModel is reclaimed and needs to be added

  • Job

    A measure of coroutines equivalent to a work task. The launch method returns a new Job by default

  • Suspend

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

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

Introduction of coroutines

Main framework ($coroutines_version replaced with the latest version, e.g. 1.3.9, likewise below)

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

LifecycleScope (Optional, version 2.2.0)

implementation 'androidx.activity:activity-ktx:$lifecycle_scope_version'
Copy the code

ViewModelScope (optional, version 2.3-beta01)

implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$coroutines_viewmodel_version"
Copy the code

Simple to use

Let’s start with a simple example

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

This example waits for 2 seconds and changes the text value of the TextView control with id tvTest to Test

Custom delayed return methods

In Kotlin, methods that require a delay to return results need to be labeled suspend

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

In other threads, if need to use Continuation thread, can use suspendCancellableCoroutine or suspendCoroutine package (the former can be cancelled, the equivalent of the latter’s extension), a successful call it. The resume (), Failure to call it.resumeWithexception (Exception()) to throw an Exception

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

Exception handling

All failures in coroutines can be handled uniformly by exception catching

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

Cancel the function

The following two jobs are executed, the first is raw and the second cancels the first job after 1 second, which causes the text of tvText to remain unchanged

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

Set the 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()
    }
}
Copy the code

Job with return value

Similar to Launch, an async method returns a Deferred object, which is an extension of Job. Deferred gets the result returned

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())
}
Copy the code

Senior advanced

Custom CoroutineScope

First look at the CoroutineScope source code

public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}
Copy the code

CoroutineScope mainly contains a coroutineContext object, we want to customize just implement coroutineContext get method

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

CoroutineContext = coroutineContext = coroutineContext = 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 {. }}Copy the code

It is essentially a collection of elements, except that unlike sets and maps, it implements its own get, fold, subtraction, and object composition (plus, Such as val coroutineContext = coroutineContext1 + coroutineContext2)

Its main content is Element, and Element’s implementation has

  • The Job tasks
  • ContinuationInterceptor interceptor
  • AbstractCoroutineContextElement
  • CoroutineExceptionHandler
  • ThreadContextElement
  • DownstreamExceptionElement
  • .

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

Job
  • Job: If the child Job is cancelled, the parent Job and other child jobs are cancelled. The parent job is cancelled and all child jobs are cancelled
  • The parent job is canceled, while all child jobs are canceled
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
}
Copy the code

We’ll be able to create instances in our activity if we want to replace the lifecycleScope for our activity

val testScope=TestScope()
Copy the code

Then cancel all jobs when the activity is destroyed

override fun onDestroy(a) {
    testScope.cancel()
    super.onDestroy()
}
Copy the code

Other uses are the same as lifecycleScope, e.g

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

In-depth understanding of Job

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

We’ll start by creating the job. By default, launch is called with three parameters: CoroutineContext, CoroutineStart, and code block.

  • Context: The CoroutineContext object, which by DEFAULT is coroutinestart. DEFAULT, collapses with the CoroutineScope context
  • Start: indicates the object of the CoroutineStart. The DEFAULT value is coroutinestart.default, indicating that the execution is immediate.LAZY indicates that the execution is not immediate
val job2= lifecycleScope.launch(start =  CoroutineStart.LAZY) {
    delay(2000)
    Log.i("scope test"."lazy")
}
job2.start()
Copy the code

When using this model to create the default state is new, so isActive isCompleted, isCancelled is false, when calling start, converted into the active state, only isActive is true, Completing the Completing task, it enters the Completing state, and waits for sub-jobs to complete. In this state, only isActive is true. Completing all sub-jobs isCompleted, and only isCompleted is true. If cancellation or abnormality occurs in active or Completing states, the Cancelling state will enter. If parent job and other child jobs need to be Cancelled, only isCancelled will be true, and finally Cancelled will be Cancelled. IsCancelled and isCompleted are 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

CancelAndJoin () and cancelAndJoin() are required for different job interactions.

  • Join () : Adds the current job to other coroutine tasks
  • CancelAndJoin () : Cancels the operation, only after it has been added
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")}Copy the code

Suspend deep understanding

Suspend is a new kotlin method modifier, and is ultimately implemented in Java, so let’s look at the differences

suspend fun test1(a){}
fun test2(a){}
Copy the code

Corresponding Java code

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

Corresponding bytecode

public final test1(Lkotlin/coroutines/Continuation;)Ljava/lang/Object; . L0 LINENUMBER6 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(a)V
   L0
    LINENUMBER 9 L0
    RETURN
   L1
    LOCALVARIABLE this Lcom/lieni/android_c/ui/test/TestActivity; L0 L1 0
    MAXSTACK = 0
    MAXLOCALS = 1

Copy the code

As you can see, the suspend method is essentially the same as the normal method, except that it is passed in with a Continuation object and returns the Unit.instance object

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

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

The implementation of continuations is in the 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}}}}... }Copy the code

When we call resumeWith, it will continue through a loop, calling invokeSuspend(param) and releaseIntercepted(), until the top-level completion execution returns and the coroutine’s interceptor is released

The final release is implemented in ContinuationImpl

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

From here the release is finally implemented through the Element of the ContinuationInterceptor in CoroutineContext

Same thing with pausing, suspendCoroutine

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

The intercepted() method of the Continuation is called by default

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

As you can see, pauses are ultimately implemented through the Element of the ContinuationInterceptor in CoroutineContext

Process Summary (Thread switching)

  • Creating a new Continuation
  • Calling the interceptContinuation method of the ContinuationInterceptor of the Context in the CoroutineScope suspends the parent task
  • Execute the subtask (in a new thread, if a thread is specified, passing in a Continuation object)
  • After execution, the user calls resume or resumeWith with the Continuation and returns the result
  • Call the CoroutineScope ContinuationInterceptor releaseInterceptedContinuation method to restore the parent task context

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 on the main thread, you will get a white screen for 2s

runBlocking { 
    delay(2000)
    Log.i("scope test"."runBlocking is completed")}Copy the code

Blocking principle: Executing runBlocking creates BlockingCoroutine by default. BlockingCoroutine executes a loop until the current Job is isCompleted

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