协程的学习笔记

协程整理的相关教程:

Kotlin协程官方教程

协程快速上手教程

Android上的Kotlin协程

将Kotlin协程与架构组件一起使用

协程基本概念

 协程就像非常轻量级的线程;线程是由系统调度的,线程切换或线程阻塞的开销都比较大。而协程是基于于线程,但是协程挂起时不需要阻塞线程,几乎是无代价的。

 协程是一种非抢占式的任务调度模式,程序可以主动挂起和恢复。

 协程的上下文切换是由用户控制。

 而Kotlin的协程的本质上是一套对线程线程进行封装的API,这套API 隐藏了异步实现细节,让我们可以用同步的方法来写异步操作。

协程的学习

如何构建一个协程

协程的官方提供的构建器以及功能

名称 结果 描述
runBlocking T 当协程运行时阻塞线程
launch Job 发起没有任何结果的协程
async Deferred 返回带有未来结果的单个值
produce ReceiveChannel 产生元素流

主要看前面三种:

    //使用runBlocking顶层函数
    runBlocking {
        //TODO 这个协程的范围就是,这个代码块。
    }

    //使用GlobalScope单例对象调用Launch方法开启协程
    GlobalScope.launch {
        //TODO 这个协程的范围就是,这个代码块。
    }
    //使用GlobalScope单例对象调用async方法开启协程,具有并发的功能
    GlobalScope.async {
        //TODO 这个协程的范围就是,这个代码块。
    }
    

runBlocking通常是适用于单元测试的场景,在的业务开发中不会使用,因为它是线程阻塞的。

GlobalScope.launch和使用runBlocking的区别在于不会线程阻塞。但是在Android开发中同样不举荐这中方法,因为它的生命周期和app的生命周期不一致。

async创建一个协程并作为Deferred的实现返回其将来的结果。使用async创建的协程具有并发的性质。

launch函数
  GlobalScope.launch {

    }

 launch 函数,它具体的含义是:创建一个新的协程,并在指定的线程上运行它。这个被创建、被运行的所谓协程是你传给 launch 的那些代码,这一段连续代码叫做一个协程。
 launch的定义:

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job 

 launch函数是CoroutineScope的扩展方法,它是在不阻塞当前线程的情况下,启动新的协程并返回一个Job。

 launch函数的三个参数:

  • context参数,协程的CoroutineScope.coroutineContext上下文的附加功能。协程上下文是从CoroutineScope继承的。可以使用context参数指定其他上下文元素。如果上下文没有任何调度程序或任何其他ContinuationInterceptor,则使用Dispatchers.Default。
  • start参数,启动协程启动选项。默认值为CoroutineStart.DEFAULT,默认情况下,协程将立即安排执行

启动模式

  // 默认,创建后立即开始调度,调度前被取消,直接进入取消响应状态。
 DEFAULT,

   // 懒加载,不会立即开始调度,需要手动调用start、join或await才会
  // 开始调度,如果调度前就被取消,协程将直接进入异常结束状态。
   LAZY,

   // 和Default类似,立即开始调度,在执行到一个挂起函数前不响应取消。
   // 涉及到cancle才有意义
   @ExperimentalCoroutinesApi
   ATOMIC,

  // 直接在当前线程执行协程体,直到遇到第一个挂起函数,才会调度到
   // 指定调度器所在的线程上执行
  @ExperimentalCoroutinesApi
   UNDISPATCHED;
  • block参数,在提供的范围的上下文中将调用的协程代码。

 在需要去切换线程或指定线程执行任务的时候,我们如何去切换线程的呢?
协程构建器 launch 和 async接收一个可选的CoroutineContext参数,就是上面所述的第一个参数。这个参数的类型是CoroutineContext协程的上下文,它包含了一个协程调度器 CoroutineDispatcher,协程调度器确定了协程在哪个线程或者哪些线程上执行,协程调度器可以将协程限制在一个特定的线程执行,或将它分派到一个线程池,亦或是让它不受限地运行。

Kotlin预置四种调度器:

  • Dispatchers.IO:IO调度器,适合执行IO相关操作,IO密集型任务调度器,针对磁盘和网络 IO 进行了优化,适合 IO 密集型的任务,比如:读写文件,操作数据库以及网络请求。
  • Dispatchers.Main:UI调度器,它仅限于与UI对象一起操作的Main线程,如Android的主线程
  • Dispatchers.Unconfined:不指定线程,如果子协程切换线程,接下来的代码也在该线程继续执行。
  • Dispatchers.Default:默认调度器,由JVM上的共享线程池支持,适合处理后台计算,CPU密集型任务调度器。适合 CPU密集型的任务。

接下来看下面的一段代码:

GlobalScope.launch(Dispatchers.Main) {
          launch(Dispatchers.Main) {
              Log.e(TAG, "Dispatchers.Main:${Thread.currentThread().name}" )
          }
          launch(Dispatchers.IO) {
              Log.e(TAG, "Dispatchers.IO:${Thread.currentThread().name}" )
          }
          launch(Dispatchers.Default) {
              Log.e(TAG, "Dispatchers.Default:${Thread.currentThread().name}" )
          }
          launch(Dispatchers.Unconfined) {
              Log.e(TAG, "Dispatchers.Unconfined:${Thread.currentThread().name}" )
          }
          launch {
              Log.e(TAG, "不传入参数:${Thread.currentThread().name}" )
          }
      }

执行的结果:

E/MainActivity@: Dispatchers.IO:DefaultDispatcher-worker-1
E/MainActivity@: Dispatchers.Default:DefaultDispatcher-worker-3
E/MainActivity@: Dispatchers.Unconfined:main
E/MainActivity@: Dispatchers.Main:main
E/MainActivity@: 不传入参数:main

 上面案例中,有些指定了协程调度器,Dispatchers.IO,Mian和Default都很容易理解。如果调用 launch { …… } 时不传参数,它从启动了它的 CoroutineScope 中承袭了上下文(以及调度器)。在上面的例子当中就是,从 GlobalScope.launch(Dispatchers.Main)的协程承袭了它的上下文,因此同样是主线线程。

 Dispatchers.Unconfined 是一个特殊的调度器且似乎也运行在 main 线程中,但实际上, 它是一种不同的机制。Dispatchers.Unconfined 协程调度器在调用它的线程启动了一个协程,但它仅仅只是运行到第一个挂起点。挂起后,它恢复线程中的协程,而这完全由被调用的挂起函数来决定。非受限的调度器非常适用于执行不消耗 CPU 时间的任务,以及不更新局限于特定线程的任何共享数据(如UI)的协程。(非受限的调度器是一种高级机制,可以在某些极端情况下提供帮助而不需要调度协程以便稍后执行或产生不希望的副作用, 因为某些操作必须立即在协程中执行。 非受限调度器不应该在通常的代码中使用。)

 这一段话说了很多东西,但是有点理解,通过下面的案例进行理解。

   GlobalScope.launch(Dispatchers.Main) {
           launch (Dispatchers.Unconfined){
               Log.e(TAG, "还没执行其他协程前处于的线程:${Thread.currentThread().name}" )
               delay(1000)
               Log.e(TAG, "执行其他协程后处于的线程: ${Thread.currentThread().name}")
           }
       }
       

执行的结果:

E/MainActivity@: 还没执行其他协程前处于的线程:main
E/MainActivity@: 执行其他协程后处于的线程: kotlinx.coroutines.DefaultExecutor

 从上面的案例中在还没执行delay()前,处于的线程还是继承外部协程的上下文,但是当执行delay()之后,处于的线程不在是主线程,而是与delay()挂起函数具有相同的调度器。这里提到了挂起函数,那什么是挂起函数呢?

suspend关键字

 suspend是Kotlin的关键字,用于定义一个挂起函数。这里挂起的对象是协程,当创建的协程执行到某个挂起函数的时候,这个协程就会被从当前线程挂起,换句来说就是这个协程从正在执行它的线程上脱离。脱离之后的协程并不是暂定什么都不干了,而是去执行挂起函数的代码。这里可能会有三个疑问?第一个当前线程接下来会发生什么事情?第二个就是协程从当前线程脱离了,那么协程执行挂起函数的代码的是在什么线程上执行?第三个挂起函数后面剩下的代码怎么办?。

Q:当前线程接下来会发生什么事情?

 A:如果该线程是一个后台线程,那么它要么被回收,要么被重新利用继续执行后台任务。如果该线程是一个Android主线程,将继续进行刷新界面的工作。

Q:协程从当前线程脱离了,那么协程执行挂起函数的代码的是在什么线程上执行?

A:这个被脱离的协程执行挂起函数所在的线程,取决于挂起函数。注意这个并不是说取决于suspend这个关键字,而是取决于这个函数的逻辑代码。suspend关键字只是起到提醒的作用。如果想要suspend定义的函数有切换线程的效果还需要依赖于withContext这个函数进行辅助。withContext()它在挂起函数起到了自动把线程切走和切回。

Q:挂起函数后面剩下的代码怎么办?

A:挂起函数执行完之后,协程为我们做的最爽的事情就是:自动把线程切换回来,协程会帮我post一个Runnable,让剩余的代码继续回到原来的线程进行工作。

 下面通过一个例子进行说明:

  override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        GlobalScope.launch(Dispatchers.Main) {
            Log.e(TAG, "onCreate:执行挂起函数之前协程所处的线程:${Thread.currentThread().name}" )
            Log.e(TAG, "onCreate: 开始执行挂起函数" )
            suspendFunction()
            Log.e(TAG, "onCreate: 执行完挂起函数协程所处的线程:${Thread.currentThread().name}" )
        }
    }

    private suspend fun suspendFunction(){
        withContext(Dispatchers.IO){
            Log.e(TAG, "suspendFunction: ${Thread.currentThread().name}" )
            Log.e(TAG, "suspendFunction: 挂起函数执行结束" )
        }
    }

执行的结果:

E/MainActivity@: onCreate:执行挂起函数之前协程所处的线程:main
E/MainActivity@: onCreate: 开始执行挂起函数
E/MainActivity@: suspendFunction: DefaultDispatcher-worker-1
E/MainActivity@: suspendFunction: 挂起函数执行结束
E/MainActivity@: onCreate: 执行完挂起函数协程所处的线程:main

  在上面的例子中,我们的协程原本是运行在主线程的,当代码遇到 suspend 函数的时候,发生线程切换,根据 Dispatchers 切换到了 IO 线程;当这个函数执行完毕后,线程又切了回来,切回来也就是协程会帮我再 post 一个 Runnable,让我剩下的代码继续回到主线程去执行。

 总的来说就是协程在执行到有 suspend 标记的函数的时候,会被 suspend 也就是被挂起,而所谓的被挂起,就是切个线程;
不过区别在于,挂起函数在执行完成之后,协程会重新切回它原先的线程。

需要注意的是,suspend 关键字只有一个效果:就是限制这个函数只能在协程里被调用,如果在非协程的代码中调用,就会编译不通过。创建一个 suspend 函数,为了让它包含真正挂起的逻辑,要在它内部直接或间接调用 Kotlin 自带的 suspend 函数。不然只是suspend在函数在加上一个关键字suspend是没有意义的。另外suspend函数要么在协程中调用,要么在另一个挂起函数中调用,不然在非协程和非挂起函数中调用,否则就会编译不通过。

async函数

 async 类似于 launch。它启动了一个单独的协程,这是一个轻量级的线程并与其它所有的协程一起并发的工作。不同之处在于 launch 返回一个 Job 并且不附带任何结果值,而 async 返回一个 Deferred。可以使用 .await() 在一个延期的值上得到它的最终结果。Deferred是Job的子类,同样具有Job的部分功能。

使用async并发

 当有两个挂起函数需要进行请求远程服务时或者需要进行大量的计算时,这个时候如果我们按如下方式调用它们:

fun main(){
    runBlocking {
        val time = measureTimeMillis {
            val one= doSomethingOne()
            val two= doSomethingTwo()
            println("结果为:${one+two}")
        }
        println("总共所要的时间:$time")
    }
}
suspend fun doSomethingOne():Int{
    //进行耗时的操作或计算
    delay(1000)
    return 1
}
suspend fun doSomethingTwo():Int{
    //进行耗时的操作或计算
    delay(1000)
    return 2
}

执行的结果:

结果为:3
总共所要的时间:2031


在上面的代码中我们顺序调用doSomethingOne和doSomethingTwo的两个挂起函数,他们在执行的时候按照默认的顺序进行执行,也就是说先执行完doSomethingOne之后,再开始执行doSomethingTwo。实际上,我们没必要这样做,如果doSomethingTwo需要依赖doSomethingOne的结果的时候我们才需要这么做。如果两个并不依赖,这样做就会花费更多的时间。这时候我们会想到并发,协程的提供了async函数,可以实现并发执行。将上面的代码进行修改:


fun main(){
    runBlocking {
        val time = measureTimeMillis {
            val one= async { doSomethingOne() }
            val two= async { doSomethingTwo() }
            println("结果为:${one.await()+two.await()}")
        }
        println("总共所要的时间:$time")
    }
}
suspend fun doSomethingOne():Int{
    //进行耗时的操作或计算
    delay(1000)
    return 1
}
suspend fun doSomethingTwo():Int{
    //进行耗时的操作或计算
    delay(1000)
    return 2
}


执行的结果

结果为:3
总共所要的时间:1013

 通过上面的两个时间相比,可以明确知道使用async并发时间快了很多。

惰性启动的async

 a sync 可以通过将 start 参数设置为 CoroutineStart.LAZY 而变为惰性的。 在这个模式下,只有结果通过 await 获取的时候协程才会启动,或者在 Job 的 start 函数调用的时候。


fun main(){
    runBlocking {
        val time = measureTimeMillis {
            val one= async(start = CoroutineStart.LAZY) { doSomethingOne() }
            val two= async(start = CoroutineStart.LAZY) { doSomethingTwo() }
            println("结果为:${one.await()+two.await()}")
        }
        println("总共所要的时间:$time")
    }
}
suspend fun doSomethingOne():Int{
    //进行耗时的操作或计算
    delay(1000)
    return 1
}
suspend fun doSomethingTwo():Int{
    //进行耗时的操作或计算
    delay(1000)
    return 2
}

执行的结果:

结果为:3
总共所要的时间:2036

 惰性的启动需要显示调用.await或者Job的start方法才会开启,这样失去了并发的效果。

 需要注意的是,如果在上面的代码中,如何有one和two其中出现了异常,那么会发生什么事情呢?如果其中一个发生异常,那么在同一作用法域的协程都会取消,其父协程也会取消。例如,如果two发生异常,one还没执行完,one会被取消,而等待中父协程也会被取消。

Job

  利用launch函数创建一个协程的时候,返回一个Job对象,代表一个协程的工作任务。有下面常用的API

/**
 * 协程状态
 */
isActive: Boolean    //是否存活
isCancelled: Boolean //是否取消
isCompleted: Boolean //是否完成
children: Sequence<Job> // 所有子作业

/**
 * 协程控制
 */
cancel()             // 取消协程
join()               // 用于等待启动的协程完成
cancelAndJoin()      // 两者结合,取消并等待协程完成
cancelChildren()     // 取消所有子协程,可传入CancellationException作为取消原因
attachChild(child: ChildJob) // 附加一个子协程到当前协程上
SupervisorJob

一个协程如果出现了异常,将上述异常传给它的父级,然后这个协程会取消它的子协程,然后取消自己,最后传递异常给他的父级。那么如果一个协程的代码中出现了异常,那么它的子协程和自身的会被取消,以及会传递到它的父协程,也就是说当其中一个协程出现了异常,很多有关系的协程都会被取消。这个种情况在某些时候,并不是我们想要的。如下图所示:

image.png

使用SupervisorJob可以解决上述问题 ,child的失败将不会影响其他的child。SupervisorJob不会取消自己或者其他的子级。而且,SupervisorJob不会传递异常,并且允许child去处理异常。
你可以用类似val uiScope = CoroutineScope(SupervisorJob())创建一个scope,这样当一个协程失败了,也不会去传递cancellation。
image.png

 参考链接:kotlin协程四

在SupervisorJob以Job不同的是重写了childCancelled的方法,它的子协程的失败或取消不会导致父协程失败,也不会影响其他子协程,因此可以实施自定义策略来处理其子协程的失败。

private class SupervisorJobImpl(parent: Job?) : JobImpl(parent) {
   override fun childCancelled(cause: Throwable): Boolean = false
}
cancel 取消协程

 可以通过协程的返回对象Job来取消协程:

fun  main(){
    runBlocking {
        val job =launch {
            repeat(1000){
                println("Job:执行了 $it")
                delay(1000)
            }
        }
        delay(3000)
        println("准备取消Job")
        job.cancel()//取消作业
        job.join()//等待Job的执行结束
        println("Job,已被取消")
    }
}

执行的结果:

Job:执行了 0
Job:执行了 1
Job:执行了 2
准备取消Job
Job,已被取消

 上面的结果中,执行了三次就停止,如果我们没有调用cancel()取消,那么协程会一直运行下去。

接下来,看一下取消协程需要注意的地方:

取消作用域会取消它的所有子协程,也就是说父协程被取消了,它的子协程都会被取消。

 通过下面的代码验证这个一点。

  runBlocking {
       val parentJob= launch {
            val child1=launch {
                repeat(1000){
                    println("child1:执行了 $it")
                    delay(1000)
                }
            }
           val child2=launch {
               repeat(1000){
                   println("child2:执行了 $it")
                   delay(1000)
               }
           }
        }

        delay(3000)
        println("准备取消父协程")
        parentJob.cancel()//取消父协程
        //查看子协程的状态
        parentJob.children.iterator().forEach {
            if (it.isCancelled){
                println("子协程被取消")
            }
        }
        parentJob.join()//等待Job的执行结束

        println("父协程,已被取消")
    }

执行的结果:

child1:执行了 0
child2:执行了 0
child1:执行了 1
child2:执行了 1
child1:执行了 2
child2:执行了 2
准备取消父协程
子协程被取消
子协程被取消
父协程,已被取消

 从上面的结果可以知道,当父协程被取消的时,它的作用域中的两个协程不再执行,也是被取消。

同一作用域中,被取消的子协程不会影响其余兄弟协程;

 在同一作用域中,平级的协程被取消不会影响到其他的同级的协程。

fun  main(){
    runBlocking {
       val parentJob= launch {
            val child1=launch {
                repeat(1000){
                    println("child1:执行了 $it")
                    delay(1000)
                }
            }
           val child2=launch {
               repeat(1000){
                   println("child2:执行了 $it")
                   delay(1000)
               }
           }
           delay(2000)
           //取消第一个子协程
           child1.cancel()
           child1.join()
           println("取消第一个子协程")
        }

    }
}

执行的结果:

child2:执行了 0
child1:执行了 1
child2:执行了 1
取消第一个子协程
child2:执行了 2
child2:执行了 3
child2:执行了 4
child2:执行了 5
.
.
.
.

 从上面的结果可以看到,当第一个子协程被取消的时候,其他的兄弟协程并没有被取消,而且继续进行自己的工作。

取消是协作的

 协程的取消是协作式的,协程不会在调用cancel()时立即停止,调用后只是进入取消中状态,只有工作完成后才会变成已取消状态,所以需要我们在代码中定期检查协程是否处于活动状态。比如下述例子:

    runBlocking {
        val startTime = System.currentTimeMillis()
        val job = launch(Dispatchers.Default) {
            var nextPrintTime = startTime
            var i = 0
            while (i < 5) { // 一个执行计算的循环,只是为了占用 CPU
                // 每秒打印消息两次
                if (System.currentTimeMillis() >= nextPrintTime) {
                    println("job: 执行了${i++} ...")
                    nextPrintTime += 500L
                }
            }
        }
        delay(1300L) // 等待一段时间
        println("准备取消协程")
        job.cancelAndJoin() // 取消一个作业并且等待它结束
        println("协程取消完成")
    }
    

执行的结果:

job: 执行了0 ...
job: 执行了1 ...
job: 执行了2 ...
准备取消协程
job: 执行了3 ...
job: 执行了4 ...
协程取消完成

从上面的结果可以看到,当调用了cancel()是协程并没有立即停止工作,而是还执行了两次。为了可以让协程在取消的时候停止,工作需要我们在代码中定期检查协程是否处于活动状态。可以使用isActive来检查状态:

fun  main(){
    runBlocking {
        val startTime = System.currentTimeMillis()
        val job = launch(Dispatchers.Default) {
            var nextPrintTime = startTime
            var i = 0
            while (isActive) {
                // 每秒打印消息两次
                if (System.currentTimeMillis() >= nextPrintTime) {
                    println("job: 执行了${i++} ...")
                    nextPrintTime += 500L
                }
            }
        }
        delay(1300L) // 等待一段时间
        println("准备取消协程")
        job.cancelAndJoin() // 取消一个作业并且等待它结束
        println("协程取消完成")
    }
}

执行结果:

job: 执行了0 ...
job: 执行了1 ...
job: 执行了2 ...
准备取消协程
协程取消完成

另外也可以通过yeid()来实现效果,yeid()可以简单理解为起当前任务(注意是任务),释放此线程的monitor让其他正在等待的任务公平的竞争,去获得执行权。

处理协程取消的副作用

当我们要在协程取消后执行某个特定的操作,比如关闭可能正在使用的资源,或者是针对取消需要进行日志打印,又或者是执行其余的一些清理代码。我们可以通过下面的方式做到这一点:

  • 检查 !isActive

可以定期地进行 isActive 的检查,那么一旦您跳出 while 循环,就可以进行资源的清理。

  • Try catch finally

因为当协程被取消后会抛出 CancellationException 异常,我们可以将挂起的任务放置于 try/catch 代码块中,然后在 finally 代码块中执行需要做的清理任务。

处于取消中状态的协程不能够挂起

 那么如果当协程被取消后需要调用挂起函数,我们需要将清理任务的代码放置于 NonCancellable CoroutineContext 中。这样会挂起运行中的代码,并保持协程的取消中状态直到任务处理完成。

fun  main(){
    runBlocking {
        val startTime = System.currentTimeMillis()
        val job = launch(Dispatchers.Default) {
            try {
                work(startTime)
            }catch (e:CancellationException){
                println("work 已取消")
            }finally {

                withContext(NonCancellable){
                    delay(1000)
                    println("清理工作。。")
                }
            }

        }
        delay(1300L) // 等待一段时间
        println("准备取消协程")
        job.cancelAndJoin() // 取消一个作业并且等待它结束
        println("协程取消完成")
    }
}

private suspend fun work(startTime: Long) {
    var nextPrintTime = startTime
    var i = 0
    while (i < 5) {
        yield()
        // 每秒打印消息两次
        if (System.currentTimeMillis() >= nextPrintTime) {
            println("job: 执行了${i++} ...")
            nextPrintTime += 500L
        }
    }
}

执行结果:

job: 执行了0 ...
job: 执行了1 ...
job: 执行了2 ...
准备取消协程
work 已取消
清理工作。。
协程取消完成

 withContext(NonCancellable)的NonCancellable它设计用于withContext函数防止取消需要执行而无需取消的代码块。

协程的取消和异常可以参考以下两篇文章:

协程中的取消和异常 | 取消操作详解

协程中的取消和异常 | 核心概念介绍

借助 scope 来取消任务

在 Kotlin 中,定义协程必须指定其 CoroutineScope 。CoroutineScope 可以对协程进行追踪,即使协程被挂起也是如此。与的调度程序 (Dispatcher) 不同,CoroutineScope 并不运行协程,它只是确保您不会失去对协程的追踪。

为了确保所有的协程都会被追踪,Kotlin 不允许在没有使用 CoroutineScope 的情况下启动新的协程。CoroutineScope 可被看作是一个具有超能力的 ExecutorService 的轻量级版本。

CoroutineScope 会跟踪所有协程,同样它还可以取消由它所启动的所有协程。这在 Android 开发中非常有用,比如它能够在用户离开界面时停止执行协程。

在Android上使用协程

 在 Android 平台上,可以将 CoroutineScope实现与用户界面相关联。这样可以避免泄漏内存或者对不再与用户相关的 Activities 或 Fragments 执行额外的工作。当用户通过导航离开某界面时,与该界面相关的 CoroutineScope 可以取消掉所有不需要的任务。而GlobalScope并不能满足这一点,如果在Activity或者Fragment中使用GrobalScope并且没有主动去取消的时候,即使Activity或Fragment已经被销毁,协程仍然在执行。这个时候我们可以使用生命周期感知的协程范围。

生命周期感知的协程范围
  • viewModelScope

在ViewModel中使用viewModelScope来创建一个协程,如果ViewModel 清除,则在此范围内启动的所有协程都会自动取消,无需手动去取消这个协程,通过使用 viewModelScope,可以确保所有的任务,包含死循环在内,都可以在不需要的时候被取消掉。

  • lifecycleScope

在Actvity或Fragment中应该使用lifecycleScope,不建议使用GlobalScope,当 LifeCycle 回调 onDestroy() 时,协程作用域 lifecycleScope 会自动取消,协程作用域 lifecycleScope 会自动取消。而且lifecycleScope还提供了一些可以指定至少在特定的生命周期之后再执行挂起函数,可以进一步减轻 View 层的负担。

  //当控制此LifecycleCoroutineScope的Lifecycle至少处于[Lifecycle.State.CREATED]状态时,
    // 才启动去执行协程。
    fun launchWhenCreated(block: suspend CoroutineScope.() -> Unit): Job = launch {
        lifecycle.whenCreated(block)
    }
    
     //当控制此* LifecycleCoroutineScope的Lifecycle至少处于[Lifecycle.State.STARTED]状态时,
    // 才启动去执行协程。
    fun launchWhenStarted(block: suspend CoroutineScope.() -> Unit): Job = launch {
        lifecycle.whenStarted(block)
    }
    
   // 当控制此LifecycleCoroutineScope的Lifecycle至少处于[Lifecycle.State.RESUMED]状态时,
   // 才启动去执行协程。
    fun launchWhenResumed(block: suspend CoroutineScope.() -> Unit): Job = launch {
        lifecycle.whenResumed(block)
    }
    
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,313评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,369评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,916评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,333评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,425评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,481评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,491评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,268评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,719评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,004评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,179评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,832评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,510评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,153评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,402评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,045评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,071评论 2 352

推荐阅读更多精彩内容