Introduction to Android Coroutines--Basic Concepts

foreword

Before, through a login request, a simple understanding of the implementation of Android coroutines . I believe that many people are like me when I first started, seeing things in a cloud of fog and not being clear. This is because we lack a basic understanding of coroutines. In this article, we will introduce the basic knowledge related to coroutines in detail.

The article is divided into the following sections:

  1. The concept of coroutines
  2. Advantages of coroutines
  3. Coroutine-related classes and functions
  4. structured concurrency

The concept of coroutines

Essentially, coroutines are lightweight threads. Coroutines run in the context of a fixed coroutine program scope (CoroutineScope and its derived subclasses), and are started together with the coroutine builder (launch, async). A coroutine has its own life cycle, and its life cycle is limited by the life cycle corresponding to the context that starts its coroutine program scope (CoroutineScope and its derived subclasses). Let's look at the following code:

fun coroutineTest(){
    
    
  //viewModelScope Viewmodel类中CoroutineScope类的衍生子类
  //launch
  viewModelScope.launch(Dispatchers.IO) {
    
     
    delay(2000L)
    //在viewModel中创建了一个新的协程使其运行在io线程并打印一行日志信息
    Log.i("viewModelScope" , "New coroutine!")
  }
}

In the above code, we created a new coroutine in ViewModel, made it run on the io thread, and printed a line of log after a delay of 2 seconds. Then the life cycle of the coroutine is limited by the life cycle of the entire ViewModel. When the ViewModel is destroyed or recycled, even if the coroutine is no longer in the main thread, it will be canceled and recycled synchronously.

It should be noted here that when using coroutines, pay attention to the scope of coroutine startup, so as to avoid synchronous destruction of coroutines that still need to work after the page is destroyed.

Advantages of coroutines

  • Lightweight: You can run multiple coroutines on a single thread, since coroutines support suspension, which does not block the thread that is running the coroutine. Suspending saves memory compared to blocking and supports multiple parallel operations.

    fun coroutineLightweight(){
          
          
        val start = System.currentTimeMillis()
        //启动100个协程,每个协程打印一行日志
        repeat(100){
          
          
            viewModelScope.launch {
          
          
                Log.i("viewModelScope" , "coroutine num is $it")
                if (it == 100){
          
          
                    Log.i("viewModelScope" , "use time is ${System.currentTimeMillis() - start}")
                }
            }
        }
    }
    
    

    Print result:

    coroutine num is 99
    use time is 41
    
    

    We created 100 coroutines and printed logs, and the total time-consuming is only 41 milliseconds, but if we use threads, if the number of startups is larger, it is estimated that there will be insufficient memory.

  • Fewer memory leaks: With complete life cycle management, the host context will be destroyed synchronously after being destroyed, which can effectively avoid the problem of memory leaks. Use structured concurrency to perform multiple operations within a single scope.

  • Built-in cancellation support: Cancellation is automatically propagated through the running coroutine hierarchy.

  • Jetpack integration: Many Jetpack libraries include extensions that provide full coroutine support. Certain libraries also provide their own coroutine scopes (ViewModel, Lifecycled...) that you can use for structured concurrency.

Coroutine-related classes and functions

  • CoroutineScope: Coroutine scope.
  • launch, async: coroutine launcher
  • Dispatchers: Scheduler, used to assign the corresponding thread corresponding to the coroutine work
  • job: the handle of the coroutine, the object generated after the coroutine is started, which is convenient for performing operations on the coroutine started in asynchronous situations (cancel, suspend, release, etc.)
  • CoroutineContext: coroutine context

Before introducing the above content, let's look at a piece of code:

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

suspend fun get(url: String) =                 // Dispatchers.Main
    withContext(Dispatchers.IO) {
    
                  // Dispatchers.IO (main-safety block)
        /* 在这里执行网络IO操作 */             // Dispatchers.IO (main-safety block)
    }                                          // Dispatchers.Main
}


In the above code, we declare two suspended functions, the first one runs on the main thread, and the second time-consuming function runs in the io thread.

CoroutineScope

CoroutineScope is the scope of the coroutine, and each coroutine must be started in the corresponding scope. A scope generally encompasses a complete lifecycle. The scope of each coroutine will track any coroutines created by launch or async within the scope, and the running coroutines can be canceled through the scope.cancel() function. In Android's KTX library, some life cycle classes provide their own CoroutineScope. For example, ViewModel has viewModelScope and Lifecycle has lifecycleScope. However, unlike the scheduler, CoroutineScope does not run coroutines.

viewModelScope creates and starts coroutines, you can refer to the implementation of Android coroutines . Of course, if you need to create your own coroutine scope (CoroutineScope) to control the life cycle of the coroutine in a specific layer of the application, you can create a CoroutineScope as follows:

class ExampleClass {
    
    

    //Job和Dispatcher被组合成CoroutineContext,构建一个新的协程作用域对象
    val scope = CoroutineScope(Job() + Dispatchers.Main)

    fun exampleMethod() {
    
    
        // 在作用域内启动一个新的协程
        scope.launch {
    
    
            // 在新的协程内执行一些挂起的函数
            fetchDocs()
        }
    }

    fun cleanUp() {
    
    
        //取消作用域以取消正在该作用域中运行的线程
        scope.cancel()
    }
}

Note that canceled scopes can no longer create coroutines. Therefore, scope.cancel() should only be called when the class controlling its lifetime is destroyed. When using viewModelScope, the ViewModel class automatically unscopes it for you in the ViewModel's onCleared() method.

launch、async

Both launch and aync are coroutine launchers, where

  • launch starts a new coroutine without returning a result to the caller. Any job that is considered "fire and forget" can be started using launch.
  • async starts a new coroutine and allows you to return a result using a suspending function called await.

Usually, we use launch to start a new coroutine from a regular function, because regular functions cannot call await. Use async only when you need to execute multiple tasks concurrently within another coroutine or within a suspending function. Also worth noting that since async expects to make a final call to await at some point, it holds the exception and rethrows it as part of the await call. This means that if you use async to start a new coroutine from a regular function, the exception will be silently discarded. These dropped exceptions do not appear in the crash metrics nor are noted in logcat. This will cause a problem in async, and the cause of the problem will become difficult to locate.

The usage of async can be seen in the structured concurrency section below

Dispatchers

Dispatchers are schedulers for coroutines, which determine which thread or threads the associated coroutines execute on. The coroutine scheduler can limit coroutine execution to a specific thread, dispatch it to a thread pool, or let it run unrestricted.

If, we want to run code outside of the main thread, we can let Kotlin coroutines perform work on the Default or IO scheduler. In Kotlin, all coroutines must run in a scheduler, even if they run on the main thread. Coroutines can suspend themselves, and the scheduler is responsible for resuming them.

Kotlin provides three schedulers for specifying where coroutines should run:

  • Dispatchers.Main - Use this dispatcher to run coroutines on the Android main thread. This scheduler should only be used to interact with the interface and perform quick work. Examples include calling suspend functions, running Android interface framework operations, and updating LiveData objects.
  • Dispatchers.IO - This dispatcher is optimized for performing disk or network I/O outside of the main thread. Examples include using Room components, reading data from or writing data to files, and performing any network operations.
  • Dispatchers.Default - This dispatcher is optimized for performing CPU-intensive work outside of the main thread. Examples of use cases include sorting lists and parsing JSON.

In addition, we can also use the newSingleThreadContext("thread name") function to create a new dependent thread for work while starting the coroutine. Note that a dedicated thread is a very expensive resource. In a real application both must be released, using the close function to release the thread when it is no longer needed, or stored in a top-level variable so that it can be reused throughout the application.

We can use the withContext(/ scheduler type /) or launch(/ scheduler type /) function to declare the dependent thread of the coroutine.

launch {
    
     // 默认情况,运行在父协程的上下文中。如果没有父协程,则运行在主线程中
    println("main                  : I'm working in thread ${Thread.currentThread().name}")
}
launch(Dispatchers.IO) {
    
     //将会运行在IO线程中
    println("IO                    : I'm working in thread ${Thread.currentThread().name}")
}
launch(Dispatchers.Default) {
    
     // 将会获取默认调度器
    println("Default               : I'm working in thread ${Thread.currentThread().name}")
}
launch(newSingleThreadContext("MyOwnThread")) {
    
     // 将使它获得一个新的专属线程
    println("newSingleThreadContext: I'm working in thread ${Thread.currentThread().name}")
} 

The withContext() function is used similarly to lanuch.

job

Job is a handle to a coroutine. Each coroutine created using launch or async returns a Job instance, which uniquely identifies the coroutine and manages its lifecycle. You can also pass a Job to a CoroutineScope to further manage its lifecycle, as the following example shows:

class ExampleClass {
    
    
    fun exampleMethod() {
    
    
        // 声明协程对象,可以通过该对象控制协程声明周期
        val job = scope.launch {
    
    
            // 新协程
        }

        if (/*取消条件*/...) {
    
    
            //取消上面启动的协程,但是并不会对启动协程的作用域产生影响
            job.cancel()
        }
    }
}

CoroutineContext

CoroutineContext contains the following elements, and defines the behavior of the coroutine through the following set of elements:

  • Job: Control the life cycle of the coroutine.
  • CoroutineDispatcher: Dispatches work to the appropriate thread.
  • CoroutineName: The name of the coroutine, which can be used for debugging.
  • CoroutineExceptionHandler: Handles uncaught exceptions.

For new coroutines created within a scope, the system allocates a new Job instance to the new coroutine while inheriting the other CoroutineContext elements from the containing scope. Inherited elements can be replaced by passing a new CoroutineContext to the launch or async function. Note that passing a Job to launch or async has no effect because the system always allocates a new instance of Job to the new coroutine.

class ExampleClass {
    
    
    val scope = CoroutineScope(Job() + Dispatchers.Main)

    fun exampleMethod() {
    
    
        //在主线程上默认作用域上启动一个新的协程
        val job1 = scope.launch {
    
    
            // 新协程的名称 = "coroutine" (默认)
        }

        // 在默认线程上启动一个新的协程,并重新定义新的协程名称
        val job2 = scope.launch(Dispatchers.Default + CoroutineName("BackgroundCoroutine")) {
    
    
            // 新协程的名称 = "BackgroundCoroutine" (重写替换)
        }
    }
}

structured concurrency

In kotlin, each coroutine has its own scope, and the coroutine has the following characteristics

  • The newly created word writing scope in the scope of the parent coroutine belongs to its child scope;
  • There is a cascading relationship between the parent coroutine and the child coroutine;
  • The parent coroutine needs to wait for the execution of all child coroutines to complete before it ends;
  • When a parent coroutine scope is actively canceled, all child coroutine scopes created within that scope are terminated.

All coroutines started in the suspend function must be stopped when the function returns a result, so we need to ensure that these coroutines complete before returning the result. With structured concurrency in Kotlin, it is possible to define a coroutineScope that starts one or more coroutines. Then, you can use await() (for a single coroutine) or awaitAll() (for multiple coroutines) to guarantee that those coroutines complete before returning a result from the function.

For example, suppose we define a coroutineScope for fetching two documents asynchronously. By calling await() on each deferred reference, we can guarantee that these two async operations complete before returning a value:

suspend fun fetchTwoDocs() =
    coroutineScope {
    
    
        val deferredOne = async {
    
     fetchDoc(1) }
        val deferredTwo = async {
    
     fetchDoc(2) }
        deferredOne.await()
        deferredTwo.await()
    }

You can also use awaitAll() on collections, as the following example shows:

suspend fun fetchTwoDocs() =        
    coroutineScope {
    
    
        val deferreds = listOf(     // 同时获取两份文档
            async {
    
     fetchDoc(1) },  // 同步返回第一个文档的结果
            async {
    
     fetchDoc(2) }   // 同步返回第二个文档的结果
        )
        deferreds.awaitAll()        // 使用awaitAll函数等待两个网络请求
    }

Although fetchTwoDocs() uses async to start a new coroutine, the function uses awaitAll() to wait for the started coroutine to complete before returning a result. Note, however, that even though we didn't call awaitAll(), the coroutineScope builder waits until all new coroutines have completed before resuming the coroutine called fetchTwoDocs.

at last

If you want to become an architect or want to break through the 20-30K salary range, then don't be limited to coding and business, but you must be able to select models, expand, and improve programming thinking. In addition, a good career plan is also very important, and the habit of learning is very important, but the most important thing is to be able to persevere. Any plan that cannot be implemented consistently is empty talk.

If you have no direction, here I would like to share with you a set of "Advanced Notes on the Eight Major Modules of Android" written by the senior architect of Ali, to help you organize the messy, scattered and fragmented knowledge systematically, so that you can systematically and efficiently Master the various knowledge points of Android development.
img
Compared with the fragmented content we usually read, the knowledge points of this note are more systematic, easier to understand and remember, and are arranged strictly according to the knowledge system.

Welcome everyone to support with one click and three links. If you need the information in the article, you can directly scan the CSDN official certification WeChat card at the end of the article to get it for free↓↓↓

PS: There is also a ChatGPT robot in the group, which can answer your work or technical questions
picture

Guess you like

Origin blog.csdn.net/YoungOne2333/article/details/131947173
Recommended