如何创建并使用一个协程:对比runBlocking、GlobalScope与自定义协程作用域

本文概览:

  • 本文从协程历史背景,延伸至协程在android工程中的使用;重点讨论了协程的创建方式,从具体使用到实现原理进行了详细对比;考虑到代码运行时的平台差异,自定了log的扩展函数;

协程印象:

  • Kotlin1.3开始引入的

  • 协程的概念:1958年就有人提过了,在js,c#,python,ruby,go,lua

    • Kotlin的协程是基于其他语言演变而来,属于语言特性
  • 协程在android平台上的作用:

    • 异步代码同步化,以同步逻辑写出异步效果

    • 处理耗时任务:避免阻塞UI主线程引发ANR

      • 但指定协程附着在main线程,同样会引起ANR
    • 保证主线程安全:确保安全的从主线程调用任何的suspend函数

如何使用协程:

  • 引入协程的库:

     //标准库(接口):协程上下文、拦截器、挂起函数
     implementation "org.jetbrains.kotlin:kotlin-stdlib:1.6.10"
     //协程的核心库(协程的具体实现):Channel、Flow、Actor、Job、拦截器、作用域
     implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.10"
     //协程在android上的一些实现:业务代码
     implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.10"
    复制代码
  • 工程示意:

    image-20220609135527604

如何创建协程:

前置铺垫:扩展日志打印函数

  • 实现效果:

    • 按照年月日时分秒打印时间;打印协程附着在那一个线程上
    • 打印具体日志信息并对运行平台进行区分
  • 代码:扩展日志打印函数

     
     package com.zero.jiangke
     ​
     import android.os.Build
     import android.util.Log
     import java.text.SimpleDateFormat
     ​
     val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
     ​
     inline fun Long.date(): String = sdf.format(this)
     inline fun isAndroid() = try {
         Build.VERSION.SDK_INT != 0
     } catch (e: ClassNotFoundException) {
         false
     }
     ​
     fun log(value: Any?) = log(value.toString())
     fun log(msg: String) = if (isAndroid()) Log.i(
         "Zero",
         "[${System.currentTimeMillis().date()}]-[${Thread.currentThread().name}] $msg"
     ) else println("[${System.currentTimeMillis().date()}]-[${Thread.currentThread().name}] $msg")
     ​
     fun main() {
         log("hello world")
     }
    复制代码
  • 运行效果:

    图片.png

协程基本创建方式:

使用runBlocking创建协程块

  • 工作机制:启动一个新的协程并阻塞调用它的线程,直到里面的代码执行完毕,返回值是泛型T

    • 通过 joinBlocking实现阻塞

      图片.png

  • joinBlocking工作机制:阻塞当前线程,知道线程执行完;将state返回

    • joinBlocking代码展示:

      image-20220609141716069

  • runBlocking代码分析:

    • 函数签名:

       runBlocking(context: CoroutineContext = EmptyCoroutineContext, block:
                   suspend CoroutineScope.() -> T): T
      复制代码
    • 参数解析:

      • CoroutineContext: 协程上下文
      • suspend CoroutineScope.() -> T: 协程体(block)
      • 返回参数: T
  • runBlocking测试代码:

     package com.zero.jiangke
     ​
     import kotlinx.coroutines.*
     import kotlin.coroutines.CoroutineContext
     import kotlin.coroutines.EmptyCoroutineContext
     ​
     fun main(){
     //    t指代这个文件,待会儿直接点就行了
         val t = CorountineTest1()
         t.start3()
     }
     ​
     class CorountineTest1 {
         //1. 任何创建一个协程:使用runBlocking创建协程块
         fun start(){
             runBlocking {
                 log("runBlocking 启动一个协程")
             }
         }
     }
    复制代码
  • 运行结果:Windows平台

    image-20220609140145314

  • 可以在android平台上跑:在MainActivity中点击事件中添加如下代码

     val t = CorountineTest1()
     t.start()
    复制代码

通过launch方式启动:看不到运行结果(Windows平台上)

  • 工作机制:

    • 启动一个协程但不会阻塞其调用线程
    • 创建一个新线程并附着运行
    • 必须要在协程作用域(CoroutineScope)中才能调用,返回值是一个Job

    image-20220609142908144

  • launch方式代码分析:

    • 代码展示:

    image-20220609142416622

    • 参数分析:

       CoroutineScope.launch(
           //协程上下文:指定在哪个线程,使用什么Job    
           context: CoroutineContext = EmptyCoroutineContext,
           //协程的启动模式
           start: CoroutineStart = CoroutineStart.DEFAULT,
           //协程执行块,返回Unit
           block: suspend CoroutineScope.() -> Unit
       ): Job//返回任务(取消、执行)
      复制代码
    • 逻辑分析:直接返回协程当没有阻塞的过程(launch来不及执行程序结束了,导致没有运行结果)

    image-20220609142440814

    • 需要看到结果:阻塞一下主线程

       Thread.sleep(1000)//阻塞主线程一秒
      复制代码
  • 测试代码:

     package com.zero.jiangke
     ​
     import kotlinx.coroutines.*
     import kotlin.coroutines.CoroutineContext
     import kotlin.coroutines.EmptyCoroutineContext
     ​
     fun main(){
     //    t指代这个文件,待会儿直接点就行了
         val t = CorountineTest1()
         t.start3()
     }
     ​
     class CorountineTest1 {
         //1. 任何创建一个协程:使用runBlocking创建协程块
         fun start(){
             GlobalScope.launch{
                 log()
             }
         }
     }
    复制代码

第三种方式:GlobalScope.async

  • 创建代码:

     GlobalScope.async {
         delay(2000)
         log("async 启动一个协程")
     }
    复制代码
  • async代码分析:

    • 代码展示:

    image-20220609143303330

    • 参数分析:

       CoroutineScope.async(
           context: CoroutineContext = EmptyCoroutineContext,
           start: CoroutineStart = CoroutineStart.DEFAULT,
           block: suspend CoroutineScope.() -> T
       ): Deferred<T>
       返回参数: Deferred  Deferred<out T> : Job
      复制代码
    • 继承关系(类似launch):DeferredCorotinue--->AbstractCorotinue

  • async与launch的不同之处

    • launch返回的是 Job

    • async返回DeferredCorotinue

      • DeferredCorotinue:实现了Deferred接口并且Deferred接口继承自Job
  • Deferred接口与Job的不同之处

    • Deferred接口中定义了await函数():用户可通过这个挂起函数可以拿到结果

       public suspend fun await(): T
      复制代码
  • 为什么launch与async在android平台上可以看到执行结果,在PC平台看不到?

    • 虽然两种方式会兴起新线程(异步执行)附着不会阻塞主线程,但因为UI主线程不会退出;CPU最终会调度到协程附着的线程上;
    • 测试代码:

       fun main(){
       //    t指代这个文件,待会儿直接点就行了
           val t = CorountineTest1()
           t.start3()
       ​
           Thread.sleep(1000)
       }
       ​
       class CorountineTest1 {
           //1. 任何创建一个协程:使用runBlocking创建协程块
           fun start(){
               log("start")
               runBlocking {
                   delay(2000)
                   log("runBlocking 启动一个协程")
               }
               GlobalScope.launch {
                   delay(2000)
                   log("launch 启动一个协程")
               }
               GlobalScope.async {
                   delay(2000)
                   log("async 启动一个协程")
               }
           }
       }
      复制代码
    • 运行结果:

      • runBlocking会阻塞UI主线程而另外两种(launch与async是异步执行的);

      • 并且对于launch与async所创建的协程,到底运行在那一个线程上,是不确定的;

        • 这进一步明确了(进/线程属于OS级别而协程则是语言特性)

        • 这个是可以指定的,通过设置协程上下文中的调度器即可

           context = Dispatchers.Main
          复制代码

      image-20220609144502297

测试协程的返回值

  • 结论:

    • 关于返回值:PC平台与android平台协程的返回不用

      • PC平台会返回 1
      • android平台返回具体的类型:并且里面是有Lamda表达式的

      image-20220609145139014

    • 关于阻塞:runBlocking会阻塞UI主线程而另外两种则不会;且发现协程到底运行在那一个线程上,是不确定了;进一步明确了(进/线程属于OS级别而协程则是语言特性)

  • 代码:

     fun main(){
     //    t指代这个文件,待会儿直接点就行了
         val t = CorountineTest1()
         t.start1()
     ​
         Thread.sleep(1000)
     }
     ​
     class CorountineTest1 {
     ​
         fun start1(){
             val runBlockingJob = runBlocking {
                 log("runBlocking 启动一个协程")
             }
             log("runBlockingJob= $runBlockingJob")
     ​
             val launchJob = GlobalScope.launch {
                 log("launch 启动一个协程")
             }
             log("launchJob= $launchJob")
     ​
             val asyncJob = GlobalScope.async {
                 log("async 启动一个协程")
             }
             log("asyncJob= $asyncJob")
         }
     }
    复制代码
  • 运行结果:

    image-20220609144902705

创建协程总结:启动协程 协程作用域范围

  • 三种基本方式+高级用法(热数据通道Channel、冷数据流Flow...)

  • runBlocking{} - 主要用于测试

    • 同样具有协程作用域的,在函数签名哪里就有

      图片.png

    • 阻塞调用者线程并兴起新附着,异步执行

    • 该方法的设计目的是让suspend风格编写的库能够在常规阻塞代码中使用,常在main方法和测试中使用。

  • GlobalScope.launch/async{} - 不推荐使用

    • launch:不会阻塞并返回 Job
    • async:不会阻塞并返回 Deferred,进一步拿结果
    • 因为销毁时机不定,因此不推荐使用;
    • 由于这样启动的协程存在启动协程的组件已被销毁但协程还存在的情况,极限情况下可能导致资源耗尽,因此并不推荐这样启动,尤其是在客户端这种需要频繁创建销毁组件的场景。

协程的其余创建方式:

  • 前置知识:

    • 协程均运行在对应的协程作用域内

    • GlobalScope继承自CoroutineScope并实现了协程上下文

      image-20220609150122860

      并实现了协程上下文

      image-20220609150339662

    • CoroutineScope是一个接口且只有一个属性(意味着只要继承自CoroutineScope并实现接口属性就可以创造一个协程作用域了)

      image-20220609150155719

  • 自定义协程作用域:

    • 方式一:代码,调用CoroutineScope,根据传入的实参返回相应的scope

       fun start3(){
           //通过 CoroutineScope函数获取自定义协程作用域
           val scope = CoroutineScope(EmptyCoroutineContext)
           //启动协程:
            scope.launch {
                   log("scope launch")
               }
               scope.async {
                   log("scope async")
               }
       }
      复制代码
      • CoroutineScope函数逻辑

        image-20220609150535407

    • 方式二:实现接口

      • 代码:当没有指定协程附着的线程时,协程到底运行在那一个线程上是不确定的

         //方式2
         class MyCoroutineScope : CoroutineScope{
             override val coroutineContext: CoroutineContext
                 get() = EmptyCoroutineContext
         ​
         }
         val myCustomScope = MyCoroutineScope()
         myCustomScope.launch {
             log("myCustomScope launch")
         }
         myCustomScope.async {
             log("myCustomScope async")
         }
        复制代码

        image-20220609151044792

猜你喜欢

转载自juejin.im/post/7107261954191785992
今日推荐