美文网首页技术二三
协程第二篇(简易版)

协程第二篇(简易版)

作者: chendroid | 来源:发表于2020-05-14 13:19 被阅读0次

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

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

    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


    相关文章

      网友评论

        本文标题:协程第二篇(简易版)

        本文链接:https://www.haomeiwen.com/subject/xmltohtx.html