美文网首页
Kotlin协程[基础的使用与分析]

Kotlin协程[基础的使用与分析]

作者: 安仔夏天勤奋 | 来源:发表于2020-04-22 14:45 被阅读0次

    协程的作用

    官方描述:协程通过将复杂性放入库来简化异步编程。程序的逻辑可以在协程中顺序地表达,而底层库会为我们解决其异步性。该库可以将用户代码的相关部分包装为回调、订阅相关事件、在不同线程(甚至不同机器)上调度执行,而代码则保持如同顺序执行一样简单。kotlin 官方中文详细文档

    协程最大的作用就是切换线程。Rxjava也可以线程切换,所以协程与Rxjava切换线程类似的,Rxjava切换是线程,而Kotlin的协程与Rxjava是不同的,协程和线程又有不同的,一个协程是一块块,每一块都有一个上下文Context,通过上下文做桥接来切换。

    比如:有A线程和B线程,有三个代码块C1、C2、C3,这些代码块在执行时,在那个线程(A线程或B线程)上切换呢,那么就由协程去控制了,如A线程执行C1、C2代码块,B线程执行C3代码块。执行的单元以协程上下文Context,这么一块一块为单位来切换的。

    可以简单地理解为,协程封装好一块块的代码在线程上执行,如封装好的一块块执行单元C1、C2在A线程上,如果要切换到B线程上,那么C1或C2的结果就以条件参数方式传入到C3中在B线程上执行。

    协程的使用

    1. 添加依赖

        //协程相关的核心库
        implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2'
        implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2'
      
    2. 创建协程

      在 kotlin 里面提供了大量的高阶函数,kotlin 中 GlobalScope 类提供了几个携程构造函数。

      • runBlocking - 不是 GlobalScope 的 API,可以独立使用,区别是 runBlocking 里面的 delay 会阻塞线程,而 launch 创建的不会。

      • launch- 创建协程。

      • async - 创建带返回值的协程,返回的是 Deferred 类。

      • withContext - 不创建新的协程,在指定协程上运行代码块。

      kotlin 在 1.3 之后要求协程必须由 CoroutineScope 创建,CoroutineScope 不阻塞当前线程,在后台创建一个新协程,也可以指定协程调度器。

    3. runBlocking:T

      首先上代码

      class MainActivity : AppCompatActivity(){
        override fun onCreate(savedInstanceState: Bundle?) {
          super.onCreate(savedInstanceState)
          setContentView(R.layout.activity_main)
          tv_name.text = "learning kotlin"
      
          Log.e("lu","主线程——${Thread.currentThread().name}_id_${Thread.currentThread().id}")
          test()
          Log.e("lu","协程执行结束。")
      }
        private fun test() = runBlocking {
          //循环10次
          repeat(10){
              Log.e("lu","协程执行$it——${Thread.currentThread().name}_id_${Thread.currentThread().id}")
              //挂起2秒
              delay(2000)
              }
            }
        }
      
        打印的结果
      E/lu: 主线程——main_id_1
      E/lu: 协程执行0——main_id_1
      E/lu: 协程执行1——main_id_1
      E/lu: 协程执行2——main_id_1
      E/lu: 协程执行3——main_id_1
      E/lu: 协程执行4——main_id_1
      E/lu: 协程执行5——main_id_1
      E/lu: 协程执行6——main_id_1
      E/lu: 协程执行7——main_id_1
      E/lu: 协程执行8——main_id_1
      E/lu: 协程执行9——main_id_1
      E/lu: 协程执行结束。
      

      得出结论:

      runBlocking启动的协程任务会阻断当前线程,直到该协程执行结束。当协程执行结束之后,页面才会被显示出来。

    4. launch:Job

      首先代码

         class MainActivity : AppCompatActivity(){
         override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_main)
         tv_name.text = "learning kotlin"
      ​
         Log.e("lu","主线程——${Thread.currentThread().name}_id_${Thread.currentThread().id}")
        //        test()
         var job = GlobalScope.launch {
         //挂起5秒
         delay(5_000)
         Log.e("lu","协程执行——${Thread.currentThread().name}_id_${Thread.currentThread().id}")
         }
         Log.e("lu","主线程执行结束。")
      
         //另一种写法   指定调度器
         Log.e("lu","主线程——${Thread.currentThread().name}_id_${Thread.currentThread().id}")
        //        test()
         var job = GlobalScope.launch(Dispatchers.Unconfined) {
         //挂起5秒
         delay(5_000)
         Log.e("lu","协程执行——${Thread.currentThread().name}_id_${Thread.currentThread().id}")
         }
         Log.e("lu","主线程执行结束。")
      
         }
        }
      
          打印的结果
       E/lu: 主线程——main_id_1
       E/lu: 主线程执行结束。
       E/GED: Failed to get GED Log Buf, err(0)
       E/lu: 协程执行——DefaultDispatcher-worker-1_id_2664
      

      launch:Job是最常用的用于启动协程的方式,它最终返回一个Job类型的对象,这个对象实际上是一个接口,它包涵了许多我们常用的函数和变量。

        //Job中的常用方法
        job.isActive//是否激活
        job.isCancelled//是否取消
        job.isCompleted//是否完成
        job.start()//启动协程,除了LAZY模式,协程都不需要手动启动
        job.cancel()//调用取消函数,取消一个协程
        job.join()//阻塞,直到某个协程执行完毕
        job.cancelAndJoin()//等待协程执行完毕,然后再取消
      

      得出结论

      从执行结果看出,launch不会阻断主线程。

    1. async

      首先上代码

       class MainActivity : AppCompatActivity(){
       override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_main)
         tv_name.text = "learning kotlin"
         GlobalScope.launch {
             var result1 = GlobalScope.async {
                 getResult1()
             }
             var result2 = GlobalScope.async {
                 getResult2()
             }
      
             var result = result1.await()+result2.await();
             Log.e("lu","result——${result}")
         }
       }
      
       private suspend fun getResult1():String{
         delay(3_000)
         return "返回结果1"
       }
       private suspend fun getResult2():String{
         delay(3_000)
         return "返回结果2"
       }
       }
      
      打印的结果
      E/lu: result——返回结果1返回结果2
      E/lu: 执行时间 3 秒
      

      asynclaunch的用法基本一样,区别在于:async的返回值类型是Deferred<T>,将最后一个封装成了该对象。async可以支持并发,此时一般都跟await一起使用。

      得出结论

      async是不阻塞线程的,也就是说getResult1getResult2是同时进行的,所以获取到result的时间是3s,而不是6s。

      async 返回的是 Deferred 类型,Deferred 继承自 Job 接口,Job有的它都有,增加了一个方法 await ,这个方法接收的是 async 闭包中返回的值,async 的特点是不会阻塞当前线程,但会阻塞所在协程,也就是挂起。

      注意:async 并不会阻塞线程,只是阻塞锁调用的协程。

    从源码分析GlobalScope.launch和GlobalScope.async

    从源码层面去分析函数的使用,这样更加让我们清楚了解其原理。

    看源码并分析GlobalScope.launch

    public fun CoroutineScope.launch(
     context: CoroutineContext = EmptyCoroutineContext,
     start: CoroutineStart = CoroutineStart.DEFAULT,
     block: suspend CoroutineScope.() -> Unit
    ): Job {
     val newContext = newCoroutineContext(context)
     val coroutine = if (start.isLazy)
     LazyStandaloneCoroutine(newContext, block) else
     StandaloneCoroutine(newContext, active = true)
     coroutine.start(start, coroutine, block)
     return coroutine
    }
    

    从方法定义中可以看出,launch(...)CoroutineScope的一个扩展函数,CoroutineScope简单来说就是协程的作用范围,CoroutineScope里定义了很多函数和变量。函数launch需要传入三个参数,返回一个Job对象,Job对象就是一个接口,也继续了CoroutineContext.Element(协程上下文元素),这些元素就是一些变量和函数,方便管理协程的一些动作,如isActive是否激活、isCancelled//是否取消、isCompleted//是否完成、cancel()//调用取消函数等等。

    launch方法有三个参数:

    1. 协程下上文:context: CoroutineContext,使用EmptyCoroutineContext

    2. 协程启动模式: start: CoroutineStart,使用CoroutineStart.DEFAULT

    3. 闭包方法体(协程体):suspend CoroutineScope.() -> Unit。定义协程内需要执行的操作,接收者是CoroutineScope

    注意:前面2个是常规参数,最后一个是个对象式函数,可以使用以kotlin的闭包的写法:() 里面写常规参数,{} 里面写函数式对象的实现

    var job = GlobalScope.launch(Dispatchers.Unconfined) {}
    var job = GlobalScope.launch {}
    

    看源码并分析GlobalScope.async

    public fun <T> CoroutineScope.async(
     context: CoroutineContext = EmptyCoroutineContext,
     start: CoroutineStart = CoroutineStart.DEFAULT,
     block: suspend CoroutineScope.() -> T
    ): Deferred<T> {
     val newContext = newCoroutineContext(context)
     val coroutine = if (start.isLazy)
     LazyDeferredCoroutine(newContext, block) else
     DeferredCoroutine<T>(newContext, active = true)
     coroutine.start(start, coroutine, block)
     return coroutine
    }
    

    从方法定义中可以看出,async(...)函数和launch(...)函数是一样,只有两个点不同,在launch()函数参数中block闭包方法体是一个Unit(无返回类型)的CoroutineScoper接收者,而要async()函数参数中block闭包方法体是一个返回类型T的CoroutineScope接收者。launch(...)函数的返回值类型是Job对象,而async(...)函数的返回值类型是Deferred<T>,但Deferred<T>是继续了Job接口,所以Deferred<T>只是对Job接口的扩展而已。

    协程上下文CoroutineContext

    在launch(...)和async(...)函数的函数式对象实现里创建(newCoroutineContext)了一个协程的上下文,使用了默认的协程调度器Dispatchers.Default。

    @ExperimentalCoroutinesApi
    public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext {
     val combined = coroutineContext + context
     val debug = if (DEBUG) combined + CoroutineId(COROUTINE_ID.incrementAndGet()) else combined
     return if (combined !== Dispatchers.Default && combined[ContinuationInterceptor] == null)
     debug + Dispatchers.Default else debug
    }
    

    协程上下文(CoroutineContext)有很多作用,包括携带参数,拦截协程执行等等,多数情况下我 们不需要自己去实现协程上下文,只需要使用现成的就好。协程的上下文有一个重要的作用就是线程切换,使用协程调度器CoroutineDispatcher 来确定哪些线程用于协程执行。设置CoroutineDispatcher协程运行的线程调度器,有 4种线程模式:

    • Dispatchers.Main:使用这个调度器在 Android 主线程上运行一个协程。可以用来更新UI 。在UI线程中执行

    • Dispatchers.IO:这个调度器被优化在主线程之外执行磁盘或网络 I/O。在线程池中执行

    • Dispatchers.Default:这个调度器经过优化,可以在主线程之外执行 cpu 密集型的工作。例如对列表进行排序和解析 JSON。在线程池中执行

    • Dispatchers.Unconfined:在调用的线程直接执行(没有指定,就是在当前线程执行)

    协程调度器CoroutineDispatcher ,可以指定默认的调度器,也可以不指定(不指定就是默认 Dispatchers.Default 模式),或者我们可以自己创建协程上下文,也就是线程池,newSingleThreadContext 单线程。

    //不指定调度器
    GlobalScope.launch {}
    //指定调度器
    GlobalScope.launch(Dispatchers.Unconfined) {}
    //自定义一个协程的上下文
    val singleThreadContext = newSingleThreadContext("嘿嘿,我是单线程")
    GlobalScope.launch(singleThreadContext) { ... }
    

    协程启动模式CoroutineStart

    直击源码:

    public enum class CoroutineStart {
     DEFAULT,
     LAZY,
     @ExperimentalCoroutinesApi
     ATOMIC,
     @ExperimentalCoroutinesApi
     UNDISPATCHED;
     //...省略部分代码
    }
    

    四种启动模式的作用:

    • DEFAULT——默认的模式,马上执行协程体。

    • LAZY——懒加载模式,只有在需要的情况下才执行。

    • ATOMIC——马上执行协程体,但在开始执行之前无法取消。

    • UNDISPATCHED——马上执行当前线程协程体,直到第一个supend调用。

    实现一个LAZY启动模式实例:

    var job = GlobalScope.launch(start = CoroutineStart.LAZY) {
     //todo 要做的事情
     Log.e("lu","协程执行时间:"+System.currentTimeMillis())
    }
    //休眠5秒,再往下走
    Thread.sleep(5_000)
    //手动启动协程
    job.start()
    

    关于Job的一些函数和变量,协程使用的launch:Job中有介绍就不过多的介绍了。

    从上面的源码中可以反映出协程的组成包括的那些要素。

    协程的组成

    • CoroutineName(指定协程名称)

    • Job(协程的生命周期,用于取消协程)

    • CoroutineDispatcher,可以指定协程运行的线程

    有了CoroutineScope之后可以通过一系列的Coroutine builders来启动协程,协程运行在Coroutine builders的代码块里面。协程的运行代码一定要跑在CoroutineScope里面。

    启动一个协程需要CoroutineScope,为什么需要?

    CoroutineScope接受CoroutineContext作为参数,CoroutineContext由一组协程的配置参数组成,可以指定协程的名称,协程运行所在线程,异常处理等等。可以通过plus操作符来组合这些参数。上面的代码指定了协程运行在主线程中,并且提供了一个Job,可用于取消协程。

    • launch 启动一个协程,返回一个Job,可用来取消协程;有异常直接抛出

    • async 启动一个带返回结果的协程,可以通过Deferred.await()获取结果;有异常并不会直接抛出,只会在调用 await 的时候抛出

    • withContext 启动一个协程,传入CoroutineContext改变协程运行的上下文。

    总结

    相关文章

      网友评论

          本文标题:Kotlin协程[基础的使用与分析]

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