Use of Kotlin synergy

Posted by snteran on Fri, 21 Jan 2022 04:51:46 +0100

preface

This is a learning record of Kotlin collaboration on the Android official website. Record the characteristics and applications of Kotlin Coroutines on Android

Overview of collaborative process

1, What is a collaborative process?

Coprocessing is a concurrent design pattern that can be used to simplify asynchronously executed code. It can help manage some time-consuming tasks to prevent time-consuming tasks from blocking the main thread. The coroutine can write asynchronous code in a synchronous way instead of the traditional callback method, making the code more readable.

2, Characteristics of collaborative process?

  • Lightweight: in fact, the lightness here is relative to thread blocking. The coroutine supports hanging. When hanging, the current thread will not be blocked, that is, "non blocking hanging". When the coroutine is hung, the thread can do other things, but can't do other things during thread blocking. Therefore, the "non blocking suspension" of the process can save system resources.

  • Fewer memory leaks: when the user closes the page, the background thread may still have running tasks. If the traditional thread is used for background requests, there may be no good way to stop the thread in time. If the process is used, the process can be stopped in time through Job::cancel, In addition, the CoroutineScope can be used to manage the CoroutineScope in a unified way. For example, the CoroutineScope can cancel the CoroutineScope started in a unified way. This is called structured concurrency. It allows our program to have fewer coordination leaks, which can be regarded as a memory leak.

  • Built in Cancellation support: Cancellation will be automatically propagated in the whole collaboration hierarchy in operation.

  • Jetpack and third-party framework support: some jetpack components such as Room and ViewModel, and the third-party framework Retrofit provide support for Kt collaboration.

About collaboration scope: collaboration must run in CoroutineScope (collaboration scope). A CoroutineScope manages one or more related collaborations. For example, there is viewModelScope under the viewModel KTX package. viewModelScope manages the processes started through it. If the viewModel is destroyed, the viewModelScope will be automatically cancelled, and the running processes started through viewModelScope will also be cancelled.

Suspend and resume

There are two concepts: suspend and resume:

  • suspend: pauses the execution of the current coroutine and saves all local variables.
  • resume: used to make the suspended coroutine continue to execute from the suspended point.

There is a suspend keyword in the coroutine, which should be distinguished from the suspend concept just mentioned. The suspend keyword just mentioned is a concept, and the suspend keyword can modify a function, but only this keyword does not suspend the coroutine. Generally, the suspend keyword is to remind the caller that the function needs to be run directly or indirectly under the coroutine, Play a role of marking and reminding.

What is the function of the tag and reminder of suspend keyword? In the past, it was difficult for developers to judge whether a method is time-consuming. If a time-consuming method is called incorrectly in the main thread, the main process will get stuck. With the suspend keyword, the creator of the time-consuming function can modify the time-consuming method with the suspend keyword, In addition, the time-consuming code is put into the IO thread by using withContext{Dispatchers.IO} and other methods inside the method. The developer only needs to call it directly or indirectly under the coroutine, so as to avoid the time-consuming task running in the main thread and causing the main thread to get stuck.

The following is an official example to illustrate the two concepts of suspend and resume:

suspend fun fetchDocs() {                             // Dispatchers.Main
    val result = get("https://developer.android.com") // Dispatchers.IO for `get`
    show(result)                                      // Dispatchers.Main
}

suspend fun get(url: String) = withContext(Dispatchers.IO) { /* ... */ }

We assume that the fetchDocs method is invoked in the association, which provides a main thread environment, such as the Dispatchers.Main specified when starting the association, and the get method executes time-consuming tasks. It uses the suspend function withContext{Dispatchers.IO} to execute the time-consuming task in the IO thread.

In the fetchDocs method, when the get method starts to make a network request, it will suspend the process. When the network request is completed, get will resume the suspended process instead of notifying the main thread with a callback.

Kotlin uses the stack frame to manage the running function and its local variables. When a coroutine is suspended, the system will copy and save the current stack frame for later use. When the coroutine is restored, the stack frame is copied back from its saved location, and then the function starts running again.

Scheduler

Kotlin coroutines must run in the dispatcher. Coroutines can suspend themselves, and the dispatcher is responsible for resuming them.

There are three types of dispatchers:

  • Dispatchers.Main: runs the coroutine on the main thread.
  • Dispatchers.IO: the dispatcher is suitable for disk or network I/O and is optimized.
  • Dispatchers.Default: the dispatcher is suitable for CPU intensive work (sorting lists and parsing JSON) and is optimized.

Start process

There are two ways to start the collaboration process:

  • Launch: start a new collaboration. The return value of launch is Job. The execution result of the collaboration will not be returned to the caller.
  • Async: start a new collaboration. The return value of async is deferred. Deferred inherits to the Job. You can obtain the execution result of the collaboration by calling Deferred::await, where await is a suspended function.

launch is usually used to start a coroutine in a regular function, because a regular function cannot call Deferred::await. async can be used to start a coroutine inside a coroutine or suspended function.

The difference between launch and async:

  1. No result is returned from the launch process; async started coroutines have returned results.
  2. If there is an exception in the launch process, it will be thrown immediately; The exception of async started coroutine will not be thrown immediately. It will not be thrown until Deferred::await is called.
  3. Async is suitable for the execution of some concurrent tasks. For example, there are such services: make two network requests, and display the request results together after the two requests are completed. This can be done using async
interface IUser {
    @GET("/users/{nickname}")
    suspend fun getUser(@Path("nickname") nickname: String): User

    @GET("/users/{nickname}")
    fun getUserRx(@Path("nickname") nickname: String): Observable<User>
}
val iUser = ServiceCreator.create(IUser::class.java)
GlobalScope.launch(Dispatchers.Main) {
    val one = async {
        Log.d(TAG, "one: ${threadName()}")
        iUser.getUser("giagor")
    }
    val two = async {
        Log.d(TAG, "two: ${threadName()}")
        iUser.getUser("google")
    }
    Log.d(TAG, "giagor:${one.await()} , google:${two.await()} ")
}

Synergetic concept

CoroutineScope

CoroutineScope will track all the collaborations it creates using launch or async, and can call scope Cancel() cancels all running coroutines under the scope. In ktx, we are provided with some defined coroutine scopes, such as viewModelScope of ViewModel and Lifecycle scope of Lifecycle, which can be viewed Android KTX | Android Developers.

viewModelScope is cancelled in the onCleared() method of ViewModel

You can create your own CoroutineScope, as follows:

class MainActivity : AppCompatActivity() {
    val scope = CoroutineScope(Job() + Dispatchers.Main)
    
    override fun onCreate(savedInstanceState: Bundle?) {
    	...
        scope.launch {
            Log.d(TAG, "onCreate: ${threadName()}") // main
            fetchDoc1()
        }
        
        scope.launch { 
            ...
        }
    }
    
    suspend fun fetchDoc1() = withContext(Dispatchers.IO) {...}
    
    override fun onDestroy() {
        scope.cancel()
        super.onDestroy()
    }
}

When creating a scope, combine Job and Dispatcher as a CoroutineContext and as the construction parameter of CoroutineScope. When scope When canceling, all collaborations opened through the scope will be automatically cancelled, and then the scope cannot be used to open the collaboration (no error will be reported, but the collaboration opening is invalid).

You can also cancel a collaboration by passing in the Job of CoroutineScope:

    val job = Job()
    val scope = CoroutineScope(job + Dispatchers.Main)

    scope.launch {...}
	...
	job.cancel()

If you cancel a collaboration process with a Job, you can't start the collaboration process with a scope.

In fact, looking at the source code, you can find coroutinescope The internal function of the cancel method is to cancel through the Job:

public fun CoroutineScope.cancel(cause: CancellationException? = null) {
    val job = coroutineContext[Job] ?: error("Scope cannot be cancelled because it does not have a job: $this")
    job.cancel(cause)
}

The cancellation of the coordination process will be introduced later.

Job

When we use launch or async to create a collaboration, we will get a job instance that uniquely identifies the collaboration and manages the lifecycle of the collaboration. Job is a bit similar to the Thread class in Java.

Some methods of Thread class in Java:

It can manage the created threads.

The Job class also has some extension functions as follows:

CoroutineContext

CoroutineContext defines the behavior of a collaboration process using the following elements:

  • Job: control the life cycle of the collaboration.
  • Coroutine dispatcher: dispatch work to the appropriate thread.
  • CoroutineName: the name of the collaboration process, which can be used for debugging.
  • CoroutineExceptionHandler: handles uncapped exceptions.

For a new collaboration created in the scope, the system will assign a new Job instance to the new collaboration, and inherit other CoroutineContext elements from the scope containing the collaboration. You can replace inherited elements by passing a new CoroutineContext to the launch or async function. Please note that passing a Job to launch or async will not have any effect, because the system will always assign a new instance of the Job to the new process.

For example:

val scope = CoroutineScope(Job() + Dispatchers.Main + CoroutineName("Top Scope"))

scope.launch(Dispatchers.IO) {
    Log.d(TAG, "onCreate: ${coroutineContext[CoroutineName]}")
}
D/abcde: onCreate: CoroutineName(Top Scope)

The newly created collaboration inherits CoroutineName and other elements from the external scope, but note that the CoroutineDispatcher element is rewritten. In the newly created collaboration, the CoroutineDispatcher element is specified as dispatchers IO.

Avoid GlobalScope

In the official documents, there are three reasons why GlobalScope is not recommended:

  • (1) Promote hard coding values If you hardcode GlobalScope, you might be hard-coding Dispatchers as well.
  • (2) Make testing very hard as your code is executed in an uncontrolled scope, you won't be able to control its execution
  • (3) You can't have a common coroutinecontext to execute for all coroutines build into the scope itself

The second and third points are explained as follows: the CoroutineScope we created can perform structured concurrent operations. For example, we can call CoroutineScope Cancel to cancel all running processes under the scope. The method of canceling is as follows:

public fun CoroutineScope.cancel(cause: CancellationException? = null) {
    val job = coroutineContext[Job] ?: error("Scope cannot be cancelled because it does not have a job: $this")
    job.cancel(cause)
}

It obtains the Job of CoroutineContext internally, and then has the cancel method of Job to cancel the collaboration. There are jobs in the CoroutineContext of the CoroutineScope we created manually, for example:

val scope = CoroutineScope(Job() + Dispatchers.Main + CoroutineName("Top Scope"))

Its construction method is:

public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
    ContextScope(if (context[Job] != null) context else context + Job())

In the construction method, if the incoming CoroutineContext does not have a Job, a Job will be created and added to the CoroutineContext. However, GlobalScope is global (singleton). Its CoroutineContext is an EmptyCoroutineContext without Job members

public object GlobalScope : CoroutineScope {
    /**
     * Returns [EmptyCoroutineContext].
     */
    override val coroutineContext: CoroutineContext
        get() = EmptyCoroutineContext
}

We are calling globalscope When launching, you can specify the CoroutineContext of the collaboration started this time. When we call globalscope When canceling (), the following error will be reported:

java.lang.IllegalStateException: Scope cannot be cancelled because it does not have a job: kotlinx.coroutines.GlobalScope@11b671b

It can be seen that the reason for the error is that GlobalScope does not have a Job.

Cancellation of collaboration

Original words of official documents:

Cancellation in coroutines is cooperative, which means that when a coroutine's Job is cancelled, the coroutine isn't cancelled until it suspends or checks for cancellation. If you do blocking operations in a coroutine, make sure that the coroutine is cancellable.

It can be concluded that:

  1. The cancellation of collaboration is collaborative
  2. When an external cancels the currently running collaboration, the collaboration will not be cancelled immediately. The collaboration will be cancelled only when one of the following two situations occurs
    • The cooperation check of this cooperation process is cancelled in cooperation, which is similar to stopping the execution of a thread (the cooperation check of the thread is required).
    • When the collaboration process is suspend ed, the collaboration process will also be cancelled.

Active inspection

for instance:

val scope = CoroutineScope(Job() + Dispatchers.IO + CoroutineName("Top Scope"))

bn1.setOnClickListener {
	scope.launch {
        Thread.sleep(2000)
        Log.d(TAG, "onCreate: $isActive")
        Log.d(TAG, "onCreate: ${threadName()},${coroutineContext[CoroutineName]?.name}")
	}
}

bn2.setOnClickListener {
	scope.cancel()
}

If we only click bn1 to start the collaboration, but do not click bn2 to cancel the collaboration, the output is

D/abcde: onCreate: true
D/abcde: onCreate: DefaultDispatcher-worker-1,Top Scope

Suppose we click bn1 to start the collaboration process and click bn2 to cancel the collaboration process immediately (at this time, the collaboration process is still during Thread.sleep), then the output is

D/abcde: onCreate: false
D/abcde: onCreate: DefaultDispatcher-worker-2,Top Scope

You can see that the isActive value of the collaboration process changes to false, but the collaboration process will still be executed (although you can't start a new collaboration process through scope later).

In the above example, scope. Has been called Cancel, but the current collaboration process is still running, indicating that the real cancellation of the collaboration process requires the cooperation within the collaboration process. One of the methods is to call the ensureActive() function. The function of ensureActive is roughly equivalent to:

if (!isActive) {
    throw CancellationException()
}

Let's modify the above example:

val scope = CoroutineScope(Job() + Dispatchers.IO + CoroutineName("Top Scope"))

bn1.setOnClickListener {
	scope.launch {
        Thread.sleep(2000)
        Log.d(TAG, "onCreate: $isActive")
        // Check whether the collaboration is cancelled
        ensureActive()
        Log.d(TAG, "onCreate: ${threadName()},${coroutineContext[CoroutineName]?.name}")
	}
}

bn2.setOnClickListener {
	scope.cancel()
}

After we click bn1 to start the collaboration, we immediately click bn2 to cancel the collaboration (at this time, the collaboration is still in the Thread.sleep period). Then the output is

D/abcde: onCreate: false

It can be seen that the ensureActive() function inside the current collaboration successfully cancels the collaboration with the external cancel operation.

Of course, cooperative cancellation can also be carried out within the collaboration process in other ways.

Collaboration pending

After the external process is cancelled, the running process will also be cancelled when it is suspend ed.

Modify the above example:

val scope = CoroutineScope(Job() + Dispatchers.IO + CoroutineName("Top Scope"))

bn1.setOnClickListener {
    scope.launch {
        Thread.sleep(2000)
        Log.d(TAG, "onCreate: $isActive")
        withContext(Dispatchers.Main) {
            Log.d(TAG, 
                  "onCreate: ${threadName()},${coroutineContext[CoroutineName]?.name}")
        }
    }    
}

bn2.setOnClickListener {
	scope.cancel()
}

If we only click bn1 to start the collaboration, but do not click bn2 to cancel the collaboration, the output is

D/abcde: onCreate: true
D/abcde: onCreate: main,Top Scope

Suppose we click bn1 to start the collaboration process and click bn2 to cancel the collaboration process immediately (at this time, the collaboration process is still during Thread.sleep), then the output is

D/abcde: onCreate: false

It can be seen that when withContext suspend s the current collaboration, the collaboration is cancelled.

kotlinx. All suspend functions in coroutines are cancelable, such as withContext and delay (in the above example, the cancellation of the collaboration can also be realized by using the delay function instead of withContext). If these hang functions are called in the association, there is no need for any additional work.

Exception handling

For exceptions in the process, you can use try catch... For capture, you can also use CoroutineExceptionHandler.

CoroutineExceptionHandler is one of CoroutineContext

Using try catch... Catch exception:

class LoginViewModel(
    private val loginRepository: LoginRepository
) : ViewModel() {

    fun login(username: String, token: String) {
        viewModelScope.launch {
            try {
                loginRepository.login(username, token)
                // Notify view user logged in successfully
            } catch (error: Throwable) {
                // Notify view login attempt failed
            }
        }
    }
}

Other pending functions

coroutineScope

Suspend function coroutinescope: create a coroutinescope and call a specific suspend block in the scope. The created coroutinescope inherits the coroutinescontext of the external scope (the Job in the coroutinescontext will be rewritten).

This function is designed for parallel deconstruction. When any subprocess of the scope fails, other subprocesses in the scope will also fail, and the scope will also fail (it feels a little structured and concurrent).

When coroutineScope is used, the external collaboration will be suspended until the code in coroutineScope and the collaboration in scope are finished, and the external collaboration of suspended function coroutineScope will resume execution.

An example:

    GlobalScope.launch(Dispatchers.Main) {
        fetchTwoDocs()
        Log.d(TAG, "Under fetchTwoDocs()")
    }

    suspend fun fetchTwoDocs() {
        coroutineScope {
            Log.d(TAG, "fetchTwoDocs: ${threadName()}")
            val deferredOne = async {
                Log.d(TAG, "async1 start: ${threadName()}")
                fetchDoc1()
                Log.d(TAG, "async1 end: ${threadName()}")
            }
            val deferredTwo = async {
                Log.d(TAG, "async2: start:${threadName()}")
                fetchDoc2()
                Log.d(TAG, "async2 end: ${threadName()}")
            }
            deferredOne.await()
            deferredTwo.await()
        }
    }

    suspend fun fetchDoc1() = withContext(Dispatchers.IO) {
        Thread.sleep(2000L)
    }

    suspend fun fetchDoc2() = withContext(Dispatchers.IO) {
        Thread.sleep(1000L)
    }
D/abcde: fetchTwoDocs: main
D/abcde: async1 start: main
D/abcde: async2: start:main
D/abcde: async2 end: main
D/abcde: async1 end: main
D/abcde: Under fetchTwoDocs()

Several concerns:

  1. Under fetchTwoDocs() does not output until fetchTwoDocs is executed
  2. The code in coroutineScope runs in the main thread
  3. async's code runs in the main thread because the scope created by coroutineScope inherits the external globalscope CoroutineContext of launch.

The above code does not call deferredone await(),deferredTwo.await() executes and outputs the same result.

suspendCoroutine

/**
 * Obtains the current continuation instance inside suspend functions and suspends
 * the currently running coroutine.
 *
 * In this function both [Continuation.resume] and [Continuation.resumeWithException] can be used either synchronously in
 * the same stack-frame where the suspension function is run or asynchronously later in the same thread or
 * from a different thread of execution. Subsequent invocation of any resume function will produce an [IllegalStateException].
 */
@SinceKotlin("1.3")
@InlineOnly
public suspend inline fun <T> suspendCoroutine(crossinline block: (Continuation<T>) -> Unit): T {
    contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
    return suspendCoroutineUninterceptedOrReturn { c: Continuation<T> ->
        val safe = SafeContinuation(c.intercepted())
        block(safe)
        safe.getOrThrow()
    }
}

Suspend coroutine is a behavior of actively suspending the process. It will give you a Continuation and let you decide when to resume the execution of the process.

reference resources

  1. Kotlin coroutines on Android | Android Developers.

Topics: kotlin