协程第二篇(简易版)

本文想要尽可能简单的说一些有关协程的知识
上篇文章太过长,内容涉及较多,没有控制好:协程初探

希望这篇会更简易一些, 主要涉及到以下内容:

  1. 协程的基本使用方式;
  2. 协程的 suspend 实现原理;
  3. 协程的部分实现原理初探
  4. 协程和 LiveData 结合使用
  5. Android 中的协程调度器;

放入一张类继承图镇楼:


常见类继承图

1. 协程的基本使用方式

1.1 CoroutineScope 是什么?

CoroutineScope 是一个接口,它为协程定义了一个范围「或者称为 作用域」,每一种协程创建的方式都是它的一个扩展「方法」。

目前常用的两种方式:

  • launch
  • async

重要!!,圈起来:

  1. Kotlin 中规定协程必须在 CoroutineScope 中运行;
  2. Kotlin 中规定协程只有在 CoroutineScope 才能被创建

所以,当我们使用协程时,必须要通过 CoroutineScope 去实现。

1.2 使用CoroutineScope 实现协程的两种方式

常见有以下两种方式:

  1. 手动实现 CoroutineScope 接口

  2. ViewModel 中实现协程

手动实现 CoroutineScope 接口

本质:我们需要在当前类中实现 CoroutineScope 接口.

Activity 里,你可以这么使用:

class MyActivity : AppCompatActivity(), CoroutineScope by MainScope() {
    override fun onDestroy() {
        cancel() // cancel is extension on CoroutineScope
    }
    
    // 实现接口后,便可以调用它的扩展方法去创建协程.
    fun showSomeData()  {
        launch {
        // <- extension on current activity, launched in the main thread
        // ... here we can use suspending functions or coroutine builders with other dispatchers
           draw(data) // draw in the main thread
        }
    }
}

或者:

class DemoTestActivity : AppCompatActivity(), CoroutineScope {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    }
    
    override val coroutineContext: CoroutineContext
        get() = MainScope().coroutineContext

    private fun test() {
        launch {
            // 新建一个协程,do something
        }
    }
}

实现该接口的注意事项:

  1. 当前的类,必须要是一个定义好的,带有生命周期的对象 -> 便于我们释放协程。

有些时候,根据需求会需要你实现 CoroutineScope, 在你定义的生命周期里,例如和 Application 的生命周期一致,在后台继续工作。

ViewModel 中实现协程

代码示例如下:

/**
 * 有关协程测试的 demo
 *
 */
class CoroutineDemoViewModel : ViewModel() {

    /**
     * 开启协程方式: 1. launch; 2. async
     */
    fun startCoroutine() {
        // viewModelScope 是 ViewModel 的一个成员变量「扩展而来」
        viewModelScope.launch(Dispatchers.IO) {
            delay(1000)

            // async
            val result = async {
                delay(2000)
            }
            result.await()
        }
    }

    suspend fun test() {
        coroutineScope {
        }
    }
}

首先, viewModelScopeViewModel 的一个扩展的成员变量,是 CoroutineScope的一个对象实例。

也就是说,在 ViewModel 中,默认帮忙开发者创建了这么一个对象,也是为了便于在 ViewModel 中使用协程。

为什么推荐在 ViewModel 中使用呢

  1. ViewModel 是具有生命周期的,跟随当前的 Activity 或者 Fragment
  2. ViewModel 本身是为了处理一些耗时操作设计的,从 UI 中剥离出来;
  3. ViewModel 在销毁时,同时会销毁它里面所有正在运行的协程;
1.3 ViewModel 自动销毁 CoroutineScope 的逻辑

ViewModel 中是会自动释放协程的,那么是如何实现的呢?

viewModelScope() 源码如下:

val ViewModel.viewModelScope: CoroutineScope
        get() {
            val scope: CoroutineScope? = this.getTag(JOB_KEY)
            if (scope != null) {
                return scope
            }
            return setTagIfAbsent(JOB_KEY,
                CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate))
        }

其中 setTagIfAbsent(xxx) 会把当前 CloseableCoroutineScope 存放在 mBagOfTags 这个 HashMap 中。

ViewModel 被销毁时会走 clear() 方法:

MainThread
final void clear() {
    mCleared = true;
    // Since clear() is final, this method is still called on mock objects
    // and in those cases, mBagOfTags is null. It'll always be empty though
    // because setTagIfAbsent and getTag are not final so we can skip
    // clearing it
    if (mBagOfTags != null) {
        synchronized (mBagOfTags) {
            for (Object value : mBagOfTags.values()) {
                // see comment for the similar call in setTagIfAbsent
                closeWithRuntimeException(value);
            }
        }
    }
    onCleared();
}

这里,会把 mBagOfTags 这个 Map 中的所有 value 取出来,做一个 close 操作,也就是在这里,对我们的 coroutinesScope 做了 close() 操作,从而取消它以及取消它里面的所有协程。

2. 协程的代码运行过程分析

首先明确几个概念:

  1. Coroutine 协程中是可以新建协程的「不断套娃」;
  2. 什么时候协程算运行结束了呢?当它运行结束,并且它所有的子协程执行结束才算结束;
  3. 同时……当里面的某个子协程发生异常时,整个协程都会停止运行,抛出异常;
  4. suspend 关键字标注的方法,只能被协程里或者另外一个 suspend方法调用;

关键字 suspend 的意义:「挂起」

  • 当代码运行到这里时,会挂起当前的协程,不在继续向下执行;
  • 直到该方法运行结束, 协程恢复,继续往运行。

下面我们使用具体的代码,看一看,具体的实现逻辑是怎样的。

2.1 协程示例代码运行

Fragment 中使用 ViewModel, 代码如下:

截图代码

testHandle() 并没有调用,因为 println("MainFragment after startCoroutines... 已经能说明问题。

ViewModel 中的代码,运行代码如下:

class CoroutinesDemoViewModel : ViewModel() {

    fun startCoroutines() {

        println("startCoroutines start current thread is ${Thread.currentThread()}")
        // viewModelScope 是 ViewModel 的一个成员变量「扩展而来」
        viewModelScope.launch(Dispatchers.IO) {
            println("start current thread is ${Thread.currentThread()}")
            delay(1000)
            println("end first delay current thread is ${Thread.currentThread()}")
            // time1 为里面代码块运行的时间
            val time1 = measureTimeMillis {
                val answer1Result = getAnswer1()
                val answer2Result = getAnswer2()
                val answerLaunchFinal = answer1Result + answer2Result
                println("answerLaunchFinal is $answerLaunchFinal")
                withContext(Dispatchers.Main) {
                    println("withContext 第一次切换到主线程 current thread is ${Thread.currentThread()}")
                    // do something post value 之类, 处理 answerLaunchFinal
                }
            }

            println(" time1  the result is $time1")
            println("time1 end 串行调用结束 current thread is ${Thread.currentThread()}")
            val time2 = measureTimeMillis {
                println("time2 内部 answer1  前面 current thread is ${Thread.currentThread()}")

                val answer1 = async {
                    println("async getAnswer1() start current thread is ${Thread.currentThread()}")
                    getAnswer1()
                }

                val answer2 = async {
                    println("async getAnswer2() start current thread is ${Thread.currentThread()}")
                    getAnswer2()
                }
                // 这种实现是并发的,更快速
                val answerFinal = answer1.await()  + answer2.await()
                println("the answer final is $answerFinal")

                withContext(Dispatchers.Main) {
                    println("withContext 第二次切换到主线程 current thread is ${Thread.currentThread()}")
                    // do something post value 之类, 处理 answerLaunchFinal
                }
            }
            println(" time2  the result is $time2")
            println("time2 end 并行调用结束 current thread is ${Thread.currentThread()}")

        }

        println("startCoroutines end current thread is ${Thread.currentThread()}")
    }
....
}

为了避免大段大段的代码引起不适

其中代码 getAnswer1()getAnswer2() 如下:

private suspend fun getAnswer1(): Int {
    println("getAnswer1() start current thread is ${Thread.currentThread()}")
    // do something
    delay(1000)
    println("getAnswer1() end current thread is ${Thread.currentThread()}")
    return 11
}

private suspend fun getAnswer2(): Int {
    println("getAnswer2() start current thread is ${Thread.currentThread()}")
    // do something
    delay(1000)
    println("getAnswer2() end current thread is ${Thread.currentThread()}")
    return 12
}

拷贝一下运行结果的代码:

I/System.out: MainFragment current thread is Thread[main,5,main]
    startCoroutines start current thread is Thread[main,5,main]
    startCoroutines end current thread is Thread[main,5,main]
I/System.out: MainFragment after startCoroutines current thread is Thread[main,5,main]
I/System.out: start current thread is Thread[DefaultDispatcher-worker-4,5,main]
I/System.out: end first delay current thread is Thread[DefaultDispatcher-worker-10,5,main]
    getAnswer1() start current thread is Thread[DefaultDispatcher-worker-10,5,main]
I/System.out: getAnswer1() end current thread is Thread[DefaultDispatcher-worker-3,5,main]
I/System.out: getAnswer2() start current thread is Thread[DefaultDispatcher-worker-3,5,main]
I/System.out: getAnswer2() end current thread is Thread[DefaultDispatcher-worker-3,5,main]
    answerLaunchFinal is 23
I/System.out: withContext 第一次切换到主线程 current thread is Thread[main,5,main]
I/System.out:  time1  the result is 2026
    time1 end 串行调用结束 current thread is Thread[DefaultDispatcher-worker-10,5,main]
    time2 内部 answer1  前面 current thread is Thread[DefaultDispatcher-worker-10,5,main]
I/System.out: async getAnswer1() start current thread is Thread[DefaultDispatcher-worker-1,5,main]
    getAnswer1() start current thread is Thread[DefaultDispatcher-worker-1,5,main]
I/System.out: async getAnswer2() start current thread is Thread[DefaultDispatcher-worker-8,5,main]
    getAnswer2() start current thread is Thread[DefaultDispatcher-worker-8,5,main]
I/System.out: getAnswer1() end current thread is Thread[DefaultDispatcher-worker-4,5,main]
I/System.out: getAnswer2() end current thread is Thread[DefaultDispatcher-worker-10,5,main]
I/System.out: the answer final is 23
I/System.out: withContext 第二次切换到主线程 current thread is Thread[main,5,main]
I/System.out:  time2  the result is 1020
I/System.out: time2 end 并行调用结束 current thread is Thread[DefaultDispatcher-worker-11,5,main]

运行结果原截图如下:

协程 log 信息

上面的全部代码以及运行结果可以暂时不全部熟悉「太长不看……」,
下面我会按照 log 和对应的代码,来说明在协程中,我们需要注意的点。

2.2 协程代码运行结果分析

1.在协程中 suspend 标注的方法会在此处等待结果的返回
该协程中的剩余代码不会继续往下走,而是会在此处等待结果返回。

从上面的 log 的可以看出;

getAnswer1() start current thread is Thread[DefaultDispatcher-worker-10,5,main]
I/System.out: getAnswer1() end current thread is Thread[DefaultDispatcher-worker-3,5,main]
//注释: suspend 的 getAnswer1() 结束后,才会运行 getAnswer2() //
I/System.out: getAnswer2() start current thread is Thread[DefaultDispatcher-worker-3,5,main]

可以看到在协程中 suspend 标注的 getAnswer1(),需要等到 getAnswer1() 结束,即 end后,才会开始运行 getAnswer2() 的代码。

结论: suspend 的作用:把当前协程挂起,等待 suspend 运行结束后,协程恢复,继续运行。

2. aync 是并发的
从上面的 log 中可以看到:

// 截图部分 log 如下:
I/System.out:  time1  the result is 2026
...
I/System.out:  time2  the result is 1020

time1time2 是两种方式所花费的时间,time2 远小于 2000, 说明 async 是异步的;

结论: aync 是并发运行的

3. 协程不会堵塞主线程

从上面的 log 中可以看到:

// 主线程 log
I/System.out: MainFragment current thread is Thread[main,5,main]
    startCoroutines start current thread is Thread[main,5,main]
    startCoroutines end current thread is Thread[main,5,main]
// 主线程 log
I/System.out: MainFragment after startCoroutines current thread is Thread[main,5,main]
// 开启协程的地方
I/System.out: start current thread is Thread[DefaultDispatcher-worker-4,5,main]

代码中,start current thread 是在 startCoroutines end current 前面的;

从这里可以看出,协程的运行不会堵塞主线程的运行。

实际上,协程不会堵塞线程的运行

结论:协程的挂起与线程的执行状态没有任何关系

Thread-A 为执行协程 coroutine-a的线程名称,当该 coroutine-a 协程被挂起时,Thread-A 可能会转去做其他事情,Thread-A 的状态与 coroutine-a的状态 没有关系。

下面我们就会说到 CoroutineDispatcher 协程调度器。

4. 调度器中 DefaultScheduler.IO 不止一个线程

首先打印的 logThread[DefaultDispatcher-worker-2,5,main] 这三项分别是什么
这是源码 Thread.toString() 方法中的返回值:

  1. 第一个参数 DefaultDispatcher-worker-2 代表的是当前线程的名字 getName().
  2. 第二个参数 5 代表的是当前线程的优先级 getPriority() 默认是 5.
  3. 第三个参数 main 代表的是当前线程属于哪个线程组。

从上面的 log 中会发现:

getAnswer2() start current thread is Thread[DefaultDispatcher-worker-8,5,main]
I/System.out: getAnswer1() end current thread is Thread[DefaultDispatcher-worker-4,5,main]
I/System.out: getAnswer2() end current thread is Thread[DefaultDispatcher-worker-10,5,main]

这三个 log 的第一项参数 getName() 均不同,分别为 DefaultDispatcher-worker-8, DefaultDispatcher-worker-4, DefaultDispatcher-worker-10,它们是不同的子线程。

结论: 调度器中 DefaultScheduler.IO 里面不止一个线程。

每一次协程的挂起和恢复,都可能会伴随着 线程的切换 : 同一个调度池中线程的切换。

5. 每次协程挂起 「suspend 函数运行先后」,再次恢复时,会切换线程

首先 delay() 是个 suspend 挂起函数,可查看它的源码。

在如下 log 中:

    getAnswer1() start current thread is Thread[DefaultDispatcher-worker-10,5,main]
I/System.out: getAnswer1() end current thread is Thread[DefaultDispatcher-worker-3,5,main]

getAnswer1() 中,在 delay() 函数前后,线程从 DefaultDispatcher-worker-10 切换为了 DefaultDispatcher-worker-3

6. suspend 函数在运行结束后,会自动切换到原来的协程调度器

coroutine-a 协程被挂起,开启 coroutine-b 协程,本质上是,先切换为 coroutine-b 所在的 协程调度器内,然后在该调度器内调度一个线程给该协程运行,当再次恢复协程 coroutine-a, 会在 coroutine-a 的调度器里面选择一个线程供协程运行。

下面这段代码:

代码withContext()

运行的结果为:

I/System.out: getAnswer2() end current thread is Thread[DefaultDispatcher-worker-3,5,main]
    answerLaunchFinal is 23
I/System.out: withContext 第一次切换到主线程 current thread is Thread[main,5,main]
I/System.out:  time1  the result is 2026
    time1 end 串行调用结束 current thread is Thread[DefaultDispatcher-worker-10,5,main]

画个图表说明一下:

代码 调度器 线程 协程
getAnswer2() IO DefaultDispatcher-worker-3 coroutine-a
withContext() 代码中 Main main thread coroutine-b
withConxt() 代码后的 println IO DefaultDispatcher-worker-10 coroutine-a

可以看到在 withContext() 后,我们并没有手动指定该协程运行的 调度器,但是,默认会切换回该协程原本的调度器。

结论:协程恢复后,会自动帮我们检测是否需要切换调度器,如果需要,则切换为原本协程的调度器,在其中线程池中选择一个线程,继续运行该协程

2.3 有关协程中重要运行节点的总结

总结一下,上述里面重要的点:

  1. suspend 的作用:把当前协程挂起,等待 suspend 运行结束后,协程恢复,继续运行;

  2. 协程 aync 是并发的;

  3. 协程的挂起与线程的执行状态没有任何关系;

  4. 调度器中 DefaultScheduler.IO 里面不止一个线程;

  5. 每次协程挂起 「suspend 函数运行先后」,再次恢复时,会切换线程;

  6. 协程恢复后,会自动帮我们检测是否需要切换调度器,如果需要,则切换为原本协程的调度器,在其中线程池中选择一个线程,继续运行该协程。

3. 协程实现的原理

在第二部分中,我们分析了实际的协程代码运行,
同时也总结出了一些有关协程代码运行中重要的点,那么……
为什么呢?是怎么实现的呢?

希望能用最简单,朴实的话表达出来,不需要源码……

要看到上述的实现原理,需要 decompile to java 后才可以看清楚。

3.1 挂起函数 suspend 的实现原理

首先注意下一个变量:**COROUTINE_SUSPENDED **
当返回值是它时,表示当前函数需要挂起,协程在这里会挂起,等待该函数运行结果返回。

首先看看 suspend 函数 decompile 后的样子

   // $FF: synthetic method
   @Nullable
   final Object getAnswer2(@NotNull Continuation $completion) {
      Object $continuation;
      label20: {
         if ($completion instanceof <undefinedtype>) {
            $continuation = (<undefinedtype>)$completion;
            if ((((<undefinedtype>)$continuation).label & Integer.MIN_VALUE) != 0) {
               ((<undefinedtype>)$continuation).label -= Integer.MIN_VALUE;
               break label20;
            }
         }

         $continuation = new ContinuationImpl($completion) {
            // $FF: synthetic field
            Object result;
            int label;
            Object L$0;

            @Nullable
            public final Object invokeSuspend(@NotNull Object $result) {
               this.result = $result;
               this.label |= Integer.MIN_VALUE;
               return TestDemo1.this.getAnswer2(this);
            }
         };
      }

      Object $result = ((<undefinedtype>)$continuation).result;
      Object var6 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
      String var2;
      boolean var3;
      switch(((<undefinedtype>)$continuation).label) {
      case 0:
         ResultKt.throwOnFailure($result);
         var2 = "getAnswer2() start current thread is " + Thread.currentThread();
         var3 = false;
         System.out.println(var2);
         ((<undefinedtype>)$continuation).L$0 = this;
         ((<undefinedtype>)$continuation).label = 1;
         if (DelayKt.delay(100L, (Continuation)$continuation) == var6) {
            return var6;
         }
         break;
      case 1:
         TestDemo1 var7 = (TestDemo1)((<undefinedtype>)$continuation).L$0;
         ResultKt.throwOnFailure($result);
         break;
      default:
         throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
      }

      var2 = "getAnswer2() end current thread is " + Thread.currentThread();
      var3 = false;
      System.out.println(var2);
      return Boxing.boxInt(12);
   }

每一个 suspend 函数,在编译成 java 后,都会有一个 switch 的代码块:

  • 在这里,如果你 suspend 函数中新增调用了 suspend 函数,则会多出来一个 case;

  • case 0 运行结束后,lable 会变成 1 等待「下次运行」会走到 switch case 1 这个里面;

  • 每次运行时 lable 都会不一样, 每次 +1 操作;

  • lable 是每次都会变化的,代表着接下来的 switch 需要执行的语句块。

有种说法是:suspend 方法编译之后,会将原来的方法体变为一个由 switch 语句构成的状态机
根据每次的状态 lable 去执行不同的代码。

那么「下次运行」意味着什么呢?

意味着编译后的 getAnswer2() 会被多次调用
而协程也正是使用了这种方式实现了「回调」。

结论:
1. 每个 suspend 方法在编译成java 后,它可能会被调用不止一次
2. 「挂起函数」的实现原理,仍然是我们熟悉的回调,只不是协程帮忙我们封装好了一套完整的回调流程

3.1.1 哪里触发了「挂起函数」的再次调用呢?

编译后的代码中,有这么一段:

$continuation = new ContinuationImpl($completion) {
     / $FF: synthetic field
     Object result;
     int label;
     Object L$0;

     @Nullable
     public final Object invokeSuspend(@NotNull Object $result) {
         this.result = $result;
         this.label |= Integer.MIN_VALUE;
         return TestDemo1.this.getAnswer2(this);
     }
};

每一个 continuation 「标志当前协程」都可以通过 invokeSuspend() 再次重新调用 getAnswer2() 方法;

invokeSuspend() 在哪里被调用的呢? 在 ContinuationImplresumeWith() 方法中被调用

resume 调用

协程的启动和再次恢复,都会调用 resume() 方法,
也是通过这种形式,实现了 getAnswer2() 在实际运行中会被调用多次,执行完它内部的逻辑;
也做到了让 getAnswer2() 看上去是顺序执行。

3.2 再来看一个问题: Continuation 是什么?

可以把它看做一个回调接口「自带了 回调环境 -> context: CoroutineContext 」而上文中,我们提到的 ContinuationImpl 正是 implement 了该接口。

Continuation 的源码如下:

public interface Continuation<in T> {
    /**
     * The context of the coroutine that corresponds to this continuation.
     */
    public val context: CoroutineContext

    /**
     * Resumes the execution of the corresponding coroutine passing a successful or failed [result] as the
     * return value of the last suspension point.
     */
    public fun resumeWith(result: Result<T>)
}

Continuation 中关键的点:

  1. context 可以记录当前协程需要在哪个回调环境中实现;通常为我们设置的「协程调度器」CoroutineDispatcher
    CoroutineDispatcher 实际上是协程为什么会自动切换原来的调度器的关键。
  2. resumeWith() 是它的关键方法,也是 ContinuationImpl 主要重写的方法,在里面实现了协程的恢复操作
3.3 「挂起函数」A 调用「挂起函数」B , 不一定 A 会挂起

在上面的代码中,其实,我们判断是否「 挂起函数」A 需要挂起,是根据:

// var6 的值是:COROUTINE_SUSPENDED
if (DelayKt.delay(100L, (Continuation)$continuation) == var6) {
            return var6;
}

就是说,只有当 return COROUTINE_SUSPENDED 时才会挂起该函数;

如果返回值 DelayKt.delay() 的返回值不是 COROUTINE_SUSPENDED, 则会顺序执行,继续执行下去。

结论:A 挂起的条件是:它调用的其他 suspend 函数返回值为 COROUTINE_SUSPENDED, 否则不会挂起。

3.4 协程对象的传递和引用

上面 3.1~3.3 简单介绍了,suspend 挂起的相关知识。

当创建一个协程时,会通过调用 resume(Unit) 启动该协程, 得到 Continuation

当协程被挂起,再次恢复时,ContinuationImpl 调用 resumeWith() 恢复协程

在协程的创建中,还存在着 ContinuationInterceptor ,对协程进行拦截,而真正的拦截,是在 ContinuationImpl.intercepted() 中的拦截。

从上面的逻辑中,我们可以梳理出这样一段话:

  1. 在协程 A 中开启协程 B
  2. 当协程 B 运行结束时,调用协程 Aresume 使得 A 开始恢复
  3. 协程 A 是被谁持有了?,实际上是协程 B

其实从下面这行代码中:

if (DelayKt.delay(100L, (Continuation)$continuation) == var6) {
    return var6;
}

这里 continuation 是当前的协程对象, 在调用 delay(100) 函数时,会把它传递给 delay() 函数内部,也就是说 delay() 中持有调用它的协程的对象。

有关协程的调度问题

Continuation 加上 ContinuationInterceptor 拦截器,这是协程调度的关键;

ContinuationInterceptor 使用了 CoroutineDispatcherinterceptContinuation 拦截了所有协程运行操作。

CoroutineDispatcher 会使用 DispatchedContinuation 再次接管 Continuation

那么,是在哪里进行协程的切换,或者说 「协程调度器的切换的」?

看一个 DispatchedContinuationresumeWith 方法:

override fun resumeWith(result: Result<T>) {
    val context = continuation.context
    val state = result.toState()
    // isDispatchNeeded 方法至关重要,是判断是否需要切换「调度器的关键」
    if (dispatcher.isDispatchNeeded(context)) {
        _state = state
        resumeMode = MODE_ATOMIC_DEFAULT
        dispatcher.dispatch(context, this)
    } else {
        executeUnconfined(state, MODE_ATOMIC_DEFAULT) {
            withCoroutineContext(this.context, countOrElement) {
                continuation.resumeWith(result)
            }
        }
    }
}

dispatcher 中有一个方法是 isDispatchNeeded(context), 参数为当前协程的 context 信息,根据我们给协程设置的调度器来决定要不要切换调度器。

continuation.context 里面是包含协程的 CoroutineDispatcher 信息的

isDispatchNeeded() 方法至关重要, 它不仅在 DispatchedContinuation.resumeWith 中调用,同时也在 resumeCancellable 调用

4. 协程和 LiveData 的结合

上述代码中,只是简单的在 ViewModel 中使用了 LiveData 作为向 UI 层透传数据变化的方式。

其实可以更进一步,因为存在 lifecycle-livedata-ktx 这个库,我们可以通过它实现更多样的代码。

依赖库:

// livedata-ktx
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'

liveData{} 包裹,基本使用如下:

注意事项:

  1. 使用 emit() 返回值
// 在 viewModel 里使用,
val bannerTest: LiveData<List<HomeBanner.BannerItemData>> = liveData(IO) {
    val result = getBannerUseCase.getWanAndroidBanner()
    if (result is Result.Success) {
        emit(result.data)
    }
}

bannerTest 被观察「viewModel.bannerTest.observe」时,liveData() 里面的协程才会开始运行。

这种方式,只会让 liveData() 里面的协程运行一次,除非你再次手动监听才会触发;
因此适用于只需要单次请求的数据。

对于一个 LiveData 对象的数据,我们可以对它进行再次操作 switchMap

/**
 * The current growZone selection.
*/
private val growZone = MutableLiveData<GrowZone>(NoGrowZone)

/**
 * A list of plants that updates based on the current filter.
*/
val plants: LiveData<List<Plant>> = growZone.switchMap { growZone ->
    if (growZone == NoGrowZone) {
        plantRepository.plants
    } else {
        plantRepository.getPlantsWithGrowZone(growZone)
    }
}

每当 growZone 发生变化时,会自动触发 growZone.switchMap 的执行, 去获取对应的信息,从而更新 plants 数据。

然后,我们在 UI 层观察该 liveData 的变化:

// 观察它的变化
viewModel.plants.observe(viewLifecycleOwner) { plants ->
    // 刷新 UI,
    adapter.submitList(plants)
}

参考资料:https://github.com/googlecodelabs/kotlin-coroutines

有时我们会在代码中这么写:

private val _bannerUILD = MutableLiveData<List<HomeBanner.BannerItemData>>()

// 对外暴露的 LD
val bannerUILD: LiveData<List<HomeBanner.BannerItemData>>
    get() = _bannerUILD

为什么要同时存在 _bannerUILDbanerUILD

可以看到 :

  1. _bannerUILD 是私有的,不对外暴露的,而 banerUILD 是对 UI 层真正暴露的 LiveData 数据;

  2. _bannerUILDMutableLiveData, 可以被修改数据的 LiveData; 而 banerUILDLiveData 不可被修改数据的 LiveData, 保证了,我们向 UI 层传递 LiveData 的安全性,外部不可修改我们的数据信息。

5. CoroutineDispatcher 的种类

在最后,还是要提到一下,我们常用的 CoroutineDispatcher 协程调度器。

CoroutineDispatcher 是协程调度器, 它的种类,都在 Dispatchers 类里面,在 Android 中有一下四类:

  1. Default: CoroutineDispatcher = createDefaultDispatcher()

    默认的调度器, 在 Android 中对应的为「线程池」。
    在新建的协程中,如果没有指定 dispatcherContinuationInterceptor 则默认会使用该 dispatcher
    线程池中会有多个线程。
    适用场景:此调度程序经过了专门优化,适合在主线程之外执行占用大量 CPU 资源的工作。用法示例包括对列表排序和解析 JSON

  2. Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher

    在主线程「UI 线程」中的调度器。
    只在主线程中, 单个线程。

    适用场景:使用此调度程序可在 Android 主线程上运行协程。此调度程序只能用于与界面交互和执行快速工作。示例包括调用 suspend 函数、运行 Android 界面框架操作,以及更新 LiveData 对象。

  3. Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined

  1. IO: CoroutineDispatcher = DefaultScheduler.IO

    IO 线程的调度器,里面的执行逻辑会运行在 IO 线程, 一般用于耗时的操作。
    对应的是「线程池」,会有多个线程在里面。IODefault 共享了线程。

    适用场景: 此调度程序经过了专门优化,适合在主线程之外执行磁盘或网络 I/O。示例包括使用 Room 组件、从文件中读取数据或向文件中写入数据,以及运行任何网络操作。

6 总结

最大的遗憾是没能详细的说明协程的调度原理。

里面有很多是属于个人理解性质的结论,如果出现错误,或不妥之处,可直接说明。

期望不会对使用到协程的兄弟们造成知识的错误引导。

参考链接:

  1. https://ethanhua.github.io/2018/12/24/kotlin_coroutines/
  2. https://johnnyshieh.me/posts/kotlin-coroutine-deep-diving/
  3. https://www.jianshu.com/p/0aaf300ac0fe
  4. https://www.kotlincn.net/docs/reference/coroutines/coroutine-context-and-dispatchers.html

等……

2020.05.13 by chendroid


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270