Kotlin coroutines and the use of Android in summary (a basic knowledge)

Insert picture description here
When learning Kotlin's coroutines, I read a lot of blogs, and I will summarize them here, which will excerpt the official website and other blog posts in large amounts.
This summary series contains many articles, starting with introducing coroutines, how to use them, comparing with RxJava, how to convert existing code to coroutine forms, and the existing three-way library of coroutines (dependency injection, image loading, permissions Request, etc.).

0 What is coroutine

Official website : https://www.kotlincn.net/docs/reference/coroutines/coroutines-guide.htmlThe
official website has a brief introduction about coroutines, which can be simply understood as a package of Java threads (using CPS + The state machine to complete the suspension and resume of the coroutine), so that developers can more easily control the scheduling of threads.

Taken from Bennyhuo's crack Kotlin coroutine (2) -some introductions to several common implementations of coroutines :

Therefore, the implementation of coroutines also has the following two types according to whether to open the corresponding call stack:

  • There are stack coroutines Stackful Coroutine: Each coroutine will have its own call stack, which is somewhat similar to the call stack of a thread. In this case, the coroutine implementation is actually very close to the thread, and the main difference is in scheduling.
  • Stackless Coroutine: The coroutine does not have its own call stack, and the state of the hanging point is implemented through a syntax such as a state machine or a closure.

Kotlin's coroutines are an implementation of stackless coroutines. Its control flow depends on the state flow of the state machine compiled by the coroutine itself. Variable storage is also achieved through closure syntax.

So Kotlin's coroutine is also called a pseudo coroutine, because it does not correspond to the unique call stack in the operating system.

You can also refer to the following articles:
Kotlin Coroutines (coroutines) complete analysis (1), coroutines introduction
Kotlin Coroutines (coroutines) complete analysis (2), in-depth understanding of coroutine suspension, recovery and scheduling
crack Kotlin coroutines (2)-Several common implementations of coroutines

scenes to be used

In the article of using Kotlin coroutines to improve application performance on the official website, the scenario of using coroutines is introduced,

On the Android platform, coroutines help solve two main problems:

  • Manage long-running tasks. If not managed properly, these tasks may block the main thread and cause your application to freeze.
  • Provide main thread security, or safely call network or disk operations from the main thread. (In other words, the caller of the method does not need to know what effect the method will have on the current main thread, regardless of whether it is time-consuming, whether an exception will be generated, and how to handle the exception, all of which are done by the method writer.)

1 Introducing coroutines in Android

implementation ‘org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2’
implementation ‘org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2’
implementation “org.jetbrains.kotlinx:kotlinx-coroutines-rx2:1.3.2”

  • kotlinx-coroutines-core provides the basic API for coroutines
  • kotlinx-coroutines-android provides an Android main thread (similar to using an io.reactivex.rxjava2: rxandroid when using RxJava )
  • kotlinx-coroutines-rx2 provides support for use with RxJava

2 An example of a coroutine

CoroutineScope(Dispatchers.Main + Job()).launch {
  val user = fetchUser() // A suspending function running in the I/O thread.
  updateUser(user) // Updates UI in the main thread.
}

private suspend fun fetchUser(): User = withContext(Dispatchers.IO) {
  // Fetches the data from server and returns user data.
}

In the above code, the keyword CoroutineScopeindicates a coroutine scope, and any pending functions must be executed within a scope.
Dispatchers.Main + Job()Represents a coroutine context CoroutineContext, including a scheduler for specifying the coroutine (that is, on which thread Resume resumes the coroutine) and a parent coroutine Job, and an exception handling logic exception handler.
launchIndicates a coroutine builder, and the coroutine body in parentheses (including the suspend function and the ordinary function).

3 Types of CoroutineScope

(1)CoroutineScope

As shown in the code above, this coroutine scope is mainly used CoroutineContextto build a Scope using a custom coroutine context , as explained above.

(2)MainScope

public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)

Through the definition of MainScope code above, it can be seen that it uses Dispatchers.Main, that is, the coroutine will run on the main thread. SupervisorJob () specifies any sub-Job in the coroutine body (can be understood as a coroutine task ) When the execution fails, it will not affect other child jobs.

(2)GlobalScope

The top-level coroutines created by this Scope will not be related to any other Job, and its life cycle follows the life cycle of the entire application, and will not be canceled prematurely, so usually this Scope should be used with caution to avoid resource and memory consumption.

4 CoroutineContext

CoroutineContext mainly contains three main elements: Dispatchers, exception handling logic CoroutineExceptionHandler and a parent coroutine task Job.

(1) Dispatchers

Dispatchers are used to specify on which thread the current coroutine body executes. There are mainly the following categories:

  • Dispatchers.Default
    Use a shared thread pool, the maximum number of threads is the number of CPU cores, the minimum is 2, the name style is Thread [DefaultDispatcher-worker-2,5, main] , suitable for CPU-intensive tasks.
  • Dispatchers.IO
    Threads are shared with Dispatchers.Default, but the number of threads is limited by kotlinx.coroutines.io.parallelism. It defaults to 64 threads or the number of cores (whichever is greater). Suitable for IO intensive tasks.
  • Dispatchers.Main
    The main thread of Android, the thread name is Thread [main, 5, main]
  • Dispatchers.Unconfined
    The coroutine scheduler is not limited to any particular thread. The coroutine first executes in the current thread and lets the coroutine resume in any thread used by the corresponding suspend function.
    An example of using Dispatchers.Unconfined, with comments explaining which thread each coroutine body part will be executed in.
CoroutineScope(Dispatchers.Unconfined).launch {
    // Writes code here running on Main thread.
    
    delay(1_000)
    // Writes code here running on `kotlinx.coroutines.DefaultExecutor`.
    
    withContext(Dispatchers.IO) { ... }
    // Writes code running on I/O thread.
    
    withContext(Dispatchers.Main) { ... }
    // Writes code running on Main thread.
}

(2) Exception handler CoroutineExceptionHandler

Generally, uncaught exceptions can only be generated by coroutines created using the launch builder. Coroutines created using async will always catch all its exceptions and represent them in the generated Deferred object.

Example 1:

try {
  CoroutineScope(Dispatchers.Main).launch {
    doSomething()
  }
} catch (e: IOException) {
  // Cannot catch IOException() here.
  Log.d("demo", "try-catch: $e")
}

private suspend fun doSomething() {
  delay(1_000)
  throw IOException()
}

In the above code, the try catch on the whole coroutine outside the bread does not work, the application will still crash.

// Handles coroutine exception here.
val handler = CoroutineExceptionHandler { _, throwable ->
  Log.d("demo", "handler: $throwable") // Prints "handler: java.io.IOException"
}

CoroutineScope(Dispatchers.Main + handler).launch {
  doSomething()
}

private suspend fun doSomething() {
  delay(1_000)
  throw IOException()
}

In the above code, use CoroutineExceptionHandler to capture the exceptions that occur in the coroutine body.

// Handles coroutine exception here.
val handler = CoroutineExceptionHandler { _, throwable ->
  // Won't print the log because the exception is "CancellationException()".
  Log.d("demo", "handler: $throwable")
}

CoroutineScope(Dispatchers.Main + handler).launch {
  doSomething()
}

private suspend fun doSomething() {
  delay(1_000)
  throw CancellationException()
}

In the above code, if the coroutine body throws a CancellationException, then the CoroutineExceptionHandler cannot be caught because throwing the CancellationException is the normal mechanism for terminating the coroutine.


val job = CoroutineScope(Dispatchers.Main).launch {
  doSomething()
}

job.invokeOnCompletion {
    val error = it ?: return@invokeOnCompletion
    // Prints "invokeOnCompletion: java.util.concurrent.CancellationException".
    Log.d("demo", "invokeOnCompletion: $error")
  }
}

private suspend fun doSomething() {
  delay(1_000)
  throw CancellationException()
}

In the above code, use the invokeOnCompletion method of the Job object to catch all exceptions, including CancellationException .

5 Job v.s. SupervisorJob

By using parent-child coroutines, structured process control can be achieved.

val parentJob1 = Job()
val parentJob2 = Job()
val childJob1 = CoroutineScope(parentJob1).launch {
    val childJob2 = launch { ... }
    val childJob3 = launch(parentJob2) { ... }
}

The relationship between the parent and child coroutines specified in the above code is shown in the following figure:
Insert picture description here
where the parent coroutine is canceled, all its child coroutines will be canceled immediately:

val parentJob = Job()
CoroutineScope(Dispatchers.Main + parentJob).launch {
    val childJob = launch {
        delay(5_000)
        
        // This function won't be executed because its parentJob is 
        // already cancelled after 1 sec. 
        canNOTBeExcecuted()
    }
    launch {
        delay(1_000)
        parentJob.cancel() // Cancels parent job after 1 sec.
    }
}

⚠️ Note:
If a child coroutine throws an exception other than CancellationException , its parent coroutine and all child coroutines will be canceled.
The parent coroutine can use cancelChildren () to cancel all its child coroutines without canceling its own coroutine body
If a job is canceled, it can no longer be used as a parent job

Job has a variety of states, we can use Job.isActive to determine whether the current coroutine is still in the Active state.
Insert picture description here
If the parent coroutine is specified using SupervisorJob, any exception to any child coroutine will not affect the execution of other child coroutines. As shown in the following code:

val parentJob = Job()
val handler = CoroutineExceptionHandler { _, _ -> }
val scope = CoroutineScope(Dispatchers.Default + parentJob + handler)
val childJob1 = scope.launch {
    delay(1_000)
    // ChildJob1 fails with the IOException().
    throw IOException()
}

val childJob2 = scope.launch {
    delay(2_000)
    // This line won't be executed due to childJob1 failure.
    canNOTBeExecuted()
}
val parentJob = SupervisorJob()
val handler = CoroutineExceptionHandler { _, _ -> }
val scope = CoroutineScope(Dispatchers.Default + parentJob + handler)
val childJob1 = scope.launch {
    delay(1_000)
    // ChildJob1 fails with the IOException().
    throw IOException()
}

val childJob2 = scope.launch {
    delay(2_000)
    // Since we use SupervisorJob() as parent job, the failure of
    // childJob1 won't affect other child jobs. This function will be 
    // executed.
    canDoSomethinghHere()
}

6 Coroutine builder

Launch and async are the two built-in coroutines, which are used to execute the coroutine body synchronously and asynchronously.
Two suspend functions are executed synchronously in the following code:

override fun onCreate(savedInstanceState: Bundle?) {
  ...

  val scope = MainScope()
  scope.launch {
    val time = measureTimeMillis {
      val one = fetchDataFromServerOne()
      val two = fetchDataFromServerTwo()
      Log.d("demo", "The sum is ${one + two}")
    }
    Log.d("demo", "Completed in $time ms")
  }
}

private suspend fun fetchDataFromServerOne(): Int {
  Log.d("demo", "fetchDataFromServerOne()")
  delay(1_000)
  return 1
}
  
private suspend fun fetchDataFromServerTwo(): Int {
  Log.d("demo", "fetchDataFromServerTwo()")
  delay(1_000)
  return 2
}

The log output is as follows:

2019-12-09 00:00:34.547 D/demo: fetchDataFromServerOne()
2019-12-09 00:00:35.553 D/demo: fetchDataFromServerTwo()
2019-12-09 00:00:36.555 D/demo: The sum is 3
2019-12-09 00:00:36.555 D/demo: Completed in 2008 ms

The following code uses async to asynchronously execute two suspend functions:

override fun onCreate(savedInstanceState: Bundle?) {
  ...
  
  val scope = MainScope()
  scope.launch {
    val time = measureTimeMillis {
      val one = async { fetchDataFromServerOne() }
      val two = async { fetchDataFromServerTwo() }
      Log.d("demo", "The sum is ${one.await() + two.await()}")
    }
    
    // Function one and two will run asynchrously,
    // so the time cost will be around 1 sec only. 
    Log.d("demo", "Completed in $time ms")
  }
}

private suspend fun fetchDataFromServerOne(): Int {
  Log.d("demo", "fetchDataFromServerOne()")
  delay(1_000)
  return 1
}

private suspend fun fetchDataFromServerTwo(): Int {
  Log.d("demo", "fetchDataFromServerTwo()")
  Thread.sleep(1_000)
  return 2
}

The log output is as follows:

2019-12-08 23:52:01.714 D/demo: fetchDataFromServerOne()
2019-12-08 23:52:01.718 D/demo: fetchDataFromServerTwo()
2019-12-08 23:52:02.722 D/demo: The sum is 3
2019-12-08 23:52:02.722 D/demo: Completed in 1133 ms

In addition to the above two builders, there is also a liveData builder for use in the ViewModel, see Kotlin coroutines and the use summary in Android (two used with Jetpack architecture components) .

Reference:
Kotlin Coroutines in Android — Basics
official website coroutine document: Coroutine Basics
official website coroutine Codelabs: Using Kotlin Coroutines in your Android App

Published 82 original articles · Like 86 · Visit 110,000+

Guess you like

Origin blog.csdn.net/unicorn97/article/details/105150935