Kotlin协程

协程是什么?

协程本质上是一个轻量级的线程

如何使用?

针对不同语言和平台使用方式可参考**在项目中使用Kotlin协程**

  • Android中使用配置

    1. app->build.gradle中引入

      implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.6'
      
  • 具体使用

    使用GlobalScopelaunch构建器来构建顶层协程

    class Test{
    	fun Fun1(){
        //创建协程并继续
        GlobalScope.launch{
    	         delay(1000L)       //模拟耗时(非阻塞等待1s)
               println("World")   //延迟1s后打印输出
    	    }
        println("Hello")          //协程已在等待(非阻塞),主线程还在继续
        Thread.sleep(2000L)       //主线程阻塞2秒保证JVM存活
    	}
    }
    //代码运行结果
    Hello
    World
    

    以上代码中,我们混用了协程非阻塞的delay(...)与阻塞的Thread.sleep(...)。这容易让我们记混哪个是阻塞哪个是非阻塞的。因此我们通过显示使用runBlocking协程构建器来实现阻塞

    fun Fun1(){
        //创建协程并继续
        GlobalScope.launch{
    	         delay(1000L)       //模拟耗时(非阻塞等待1s)
               println("World")   //延迟1s后打印输出
    	    }
        println("Hello")          //协程已在等待(非阻塞),主线程还在继续
        //Thread.sleep(2000L)     //主线程阻塞2秒保证JVM存活
        runBlocking{
          delay(2000L)  //通过runBlocking协程构建器实现显示阻塞,效果等同于Thread.sleep
        }
    	}
    

    上述代码我们通过阻塞主线程延迟一段时间来等待协程运行结束,显然不是一个好选择,我们通过显示(非阻塞方式)等待所启动的后台job执行结束

     //创建协程并赋值给一个变量
        val obj = GlobalScope.launch{
    	         delay(1000L)       //模拟耗时(非阻塞等待1s)
               println("World")   //延迟1s后打印输出
    	    }
        println("Hello")          //协程已在等待(非阻塞),主线程还在继续
        obj.join()                //通过调用创建携程对象的join方法,等待直到子协程执行结束。
    现在,结果是一样的,但是无需手动设置阻塞时间。主协程与后台作业的持续时间没有任何关系。
    

结构化并发(runBlocking)

上面我们简单的介绍了协程的使用,当我们使用GlobalScope.launch时,会创建一个顶级协程,虽然轻量但仍会耗费一些资内存资源。如果忘记保持对新启动协程的引用(job = GolbalScope.luanch{}),它还会继续运行。

因此,我们通过在代码中使用解构化并发,在指定的作用域内启动协程执行操作,而不是像通常使用线程(线程总是全局的)那样在GlobalScpde中启动。结构化并发可以保证当一个作用域被取消,作用域里面的所有协程会被取消。

代码示例

我们使用runBlocking协程构建器将main函数转换为协程。包括runBlocking在内的每个协程构建器都将CoroutineScope的实例添加到其代码块所在的作用域中,我们可以在这个作用域内启动协程而无需显示的join(),因为外部协程(runBlocking)直到在其作用域中启动的所有协程都执行完毕才会结束。

注意runBlocking是创建一个新的协程同时阻塞当前线程,直到协程结束。这个不应该在协程中使用,主要是为main函数和测试设计的

/**
 *通过GlobalScope.launch创建顶级协程
 */
fun main(){
  val job = GlobalScope.launch{    
    delay(1000L)
    println("World")
  }
  println("Hello")
  //通过协程的join方法来确保启动的协程完成操作后结束
  job.join()                      
}

/**
 *优化,通过runBlocking协程构建器将函数转换为协程
 */
fun main() = runBlocking{
  //在runBlocking作用域中创建一个新协程
  launch{     
    delay(1000L)
    println("World")
  }
  println("Hello")
}

runBlocking方法会阻塞当前线程来等待作用域内所有的子协程运行完毕,runBlocking是一个常规函数。

fun test(){
  test1()
  println("main thread")
}

fun test1() = runBlocking{
  launch{
    delay(2000L)
    println("runBlocking launch")
  }
}

//运行结果
runBlocking launch
main thread

在主线程中调用runBlocking协程函数,协程函数内延迟了两秒后才执行主线程中指令,很显然runBlocking确实阻塞了当前线程。

作用域构建器(CoroutineScope)

除了由不同构建器(比如:runBlocking)提供协程作用域之外,还可以使用coroutineScope构建器声明自己的作用域。它会创建一个协程作用域并且在所有已启动的子协程执行完毕之前不会结束。

runBlockingcoroutineScope可能看起来很类似,因为它们都会等待其协程体以及所有子协程结束,两者的主要区别在于:

  • runBlocking方法会阻塞当前线程来等待。是一个常规函数;
  • coroutineScope只是挂起协程不会阻塞线程,会释放底层线程用于其他用途,是挂起函数
fun test(){
  test1()   
  //④
  println("main thread")   
}

//runBlocking协程构建器会阻塞当前线程
fun test1() = runBlocking {
  launch{
     println("runBlockig launch1")
   }
  println("runBlocking1")
  /**
   *coroutineScope 挂起函数不会阻塞当前线程,执行完coroutine Scope1之后,挂起该协程,释放线程使其
   *返回到runBlocking继续执行打印runBlockig launch1
   */
  coroutineScope{  
    launch{
      //②
      println("coroutine Scope launch1")   
    }
    //①
    println("coroutine Scope1")  
  }
  
  //③ 上面coroutineScope挂起函数完全执行结束后,才会继续后续指令
  println("runBlocking2")
  
  
  //重复上述代码块(再启动一个新协程和一个挂起函数,执行逻辑和上面分析一致)
  launch{
     println("runBlockig launch2")
  }
  coroutineScope{  
    launch{
      println("coroutine Scope launch2")   
    }
    println("coroutine Scope2")  
  }
}

//运行结果
runBlocking1
coroutine Scope1
runBlockig launch1
coroutine Scope launch1
runBlocking2
coroutine Scope2
runBlockig launch2
coroutine Scope launch2
main thread

代码执行逻辑分析:

  • ①—>launch不允许线程执行runBlocking快之后指令 ,但允许执行此launch块之后的指令,这就是coroutine Scope1runBlockig launch1之前打印原因;
  • ②—>在runBlocking上下文中嵌套的coroutineScope挂起函数,当线程遇到coroutineScope挂起函数时,会挂起当前协程,释放底层线程,使线程回到runBlocking代码中,因此runBlockig launch1coroutine Scope launch1之前打印;
  • ③—>在runBlocking上下文中嵌套的coroutineScope挂起函数,当线程遇到coroutineScope挂起函数时,将不允许线程执行此coroutineScope挂起函数块之后的指令,因此runBlocking2总是在coroutine Scope launch1之后打印。
  • ④—>runBlocking阻塞当前调用它的线程,直到内部协程完成为止。因此main threadtest1()函数执行结束之后最后才会被打印;

提取函数重构(suspend)

我们将launch{...}内部的代码块提取到独立的函数中,你会得到一个带有suspend修饰符的新函数,即挂起函数。在协程内可以像普通函数一样使用挂起函数

fun test() = runBlocking{
  launch{
    doWork()   //调用挂起函数
  }
  
  launch{
    delay(1000L)
    println("funBlocking launch1")
    coroutineScope{
      println("coroutineScope1")
      delay(2000L)
      println("coroutineScope2")
    }
  }
}

//将launch内代码块提取到独立函数中,成为一个挂起函数
suspend fun doWork(){
  delay(2000L)
  println("to to ...")
}

suspend修饰符特点

  • 用于修饰函数(表示该函数为挂起函数)
  • 不会造成线程阻塞(会挂起协程)
  • 只能在协程中使用
fun main() = runBlocking{
  //启动协程1
  launch{
    println("runBlocking launch1")
  }
  //启动协程2
  launch{
     println("runBlocking launch2")
  }
  //调用挂起函数,不会造成线程阻塞,会挂起协程
  test()
}
suspend fun test(){
  println("befor delay in suspend function")
  delay(2000L)    //挂起协程
  println("after delay in suspend function")
}

//运行结果
befor delay in suspend function
runBlocking launch1
runBlocking launch2
after delay in suspend function

代码解释

runBlocking上下文中嵌套的挂起函数,当线程遇到挂起函数时,会挂起当前协程,释放底层线程,使线程回到runBlocking代码中,因此runBlocking launch1runBlocking launch2after delay in suspend function之前打印;

取消与超时(cancelAndJoin&withTimeOut)

取消协程执行

当我们的协程不在需要的时候(比如:用户关闭了一个启动协程的界面,那么协程执行结果已经不再被需要),这时它应该可以被取消,launch返回一个可用来取消运行中的协程job

fun test() =runBlocking{
  val job = launch{
    delay(5000)
    println("Hello")
  }
  delay(2000)          //延迟2s
  job.cancelAndJoin()  //取消并等待结束。等价于job.cancel()与job.join()组合
}
//运行后,延迟两秒后协程被取消,不会打印Hello

注意:如果协程正在执行计算任务,并且没有检查取消的话,那么它是不能被取消的,就如如下示例代码所示:

val job = launch(Dispatchers.Default) {
    var nextPrintTime = startTime
    var i = 0
    while (i < 5) { // 一个执行计算的循环,只是为了占用 CPU
        // 每秒打印消息两次
        if (System.currentTimeMillis() >= nextPrintTime) {
            println("job: I'm sleeping ${i++} ...")
            nextPrintTime += 500L
        }
    }
}
delay(1300L) // 等待一段时间
println("main: I'm tired of waiting!")
job.cancelAndJoin() // 取消一个作业并且等待它结束
println("main: Now I can quit.")

以上代码在调用取消后, 作业仍然执行了五次循环迭代并运行到了它结束为止。

如何操作使正在操作的协程可取消呢?可通过CoroutineScope中的扩展属性isActive来实现

//我们将上述的while(i<5)替换为while(isActive),当调用job.canel(),协程会检查当前的状态
val job = launch(Dispatchers.Default) {
    var nextPrintTime = startTime
    var i = 0
    while (isActive) { // 可以被取消的计算循环
        // 每秒打印消息两次
        if (System.currentTimeMillis() >= nextPrintTime) {
            println("job: I'm sleeping ${i++} ...")
            nextPrintTime += 500L
        }
    }
}
delay(1300L) // 等待一段时间
println("main: I'm tired of waiting!")
job.cancelAndJoin() // 取消该作业并等待它结束
println("main: Now I can quit.")

经过使用isActive来使得运行中的协程也可被取消

超时

在取消协程时候,有可能会超时抛出异常的

withTimeout(1300L) {
    repeat(1000) { i ->
        println("I'm sleeping $i ...")
        delay(500L)
    }
}

延迟500毫秒循环1000次,当超过1300毫秒时withTimeOut抛出TimeoutCancellationException

由于取消只是一个例外,所有的资源都使用常用的方法来关闭。 如果你需要做一些各类使用超时的特别的额外操作,可以使用类似 withTimeoutwithTimeoutOrNull 函数,并把这些会超时的代码包装在 try {...} catch (e: TimeoutCancellationException) {...} 代码块中,而 withTimeoutOrNull 通过返回 null 来进行超时操作,从而替代抛出一个异常:

以下两段代码运行时不再抛出异常:

val result = withTimeoutOrNull(1300L) {
    repeat(1000) { i ->
        println("I'm sleeping $i ...")
        delay(500L)
    }
}
println("result is $result") 

//运行结果
I'm sleeping 0 ... 
I'm sleeping 1 ... 
I'm sleeping 2 ... 
Result is null

结合try…catch捕获

val result = withTimeOutOrNull(1300L){
  try{
    repeat(1000){i->
      println("I'm sleeping $i ...")
      delay(500L)
    }
  }catch(e:TimeOutCancellationException){
    println("超时")
  }
}
println("result is $result")

//运行结果
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
超时
result is null

组合挂起函数

同一个协程中代码执行默认都是顺序(同步)的,比如一个协程内调用两个挂起函数

fun main() = runBlocking {
  val time = measureTimeMillis {
    val num1 = fun1()   //先执行fun1挂起函数,等待执行结束在执行fun2挂起函数
    val num2 = fun2()
    println("num = $num1 + $num2")
    println("完成耗时:$time")
  }
}

//挂起函数1
private suspend fun fun1(): Int {
    delay(2000L)
    println("挂起函数1")
    return 10
}

//挂起函数1
private suspend fun fun2(): Int {
    delay(1000)
    println("挂起函数2")
    return 20
}
//运行结果
挂起函数1
挂起函数2
num = 30
完成耗时:3057
使用async实现异步并发

如果我们想在同一个协程内并发(异步)的运行多个挂起函数怎么办呢?可通过**async**实现并发

async类似于launch。它启动了一个单独的协程,这是一个轻量级的线程,并与其他所有的协程一起并发工作。

不同在于:

  • launch返回一个Job并且不附带任何结果值
  • async返回一个Deferred一个轻量级的非阻塞future,代表一个将会在稍后提供结果的promise。可通过使用.await()在一个延迟的值上得到它的最终结果,但是Deferred也是一个Job,所以需要的话,你也可以取消它。

注意使用协程进行并发总是显式的

使用async实现异步并发

//挂起函数1
private suspend fun fun1(): Int {
   delay(2000L)
   println("挂起函数1")
   return 10
}

//挂起函数2
private suspend fun fun2(): Int {
   delay(1000)
   println("挂起函数2")
   return 20
}

/**
 *使用async实现异步
 */
fun main() = runBlocking{
  val time = measureTimeMillis {
      //通过async启动一个独立的协程来运行挂起函数,进行并发工作,并返回一个Deferred
      val deferred1 = async { fun1() }  
      val deferred2 = async { fun2() }
      //通过deferred的await方法获取延迟执行结果
       println("num = ${deferred1.await() + deferred2.await()}") 
   }
   println("完成耗时:$time")
}
//异步运行结果
挂起函数2
挂起函数1
num = 30
完成耗时:2045

可见,使用async实现一个协程内并发操作多个挂起函数,挂起函数不再是顺序执行,而是并发执行,相比较同步执行耗时明显减少。

注意async{...}.await()会导致同步,不建议这种使用,如果想实现同步取得结果使用withContext替换

/**
 *使用async{...}.await()导致同步(冗余异步),不建议这种使用
 */
fun main() = runBlocking{
  val time1 = measureTimeMillis {
      //使用async创建一个子协程并直接使用await获取结果,导致asyc变成同步
      val value1 = async { fun1() }.await()  
      val value2 = async { fun2() }.await()
      //通过deferred的await方法获取延迟执行结果
       println("num = ${value1 + value2}") 
   }
   println("完成耗时:$time")
}
挂起函数1
挂起函数2
num = 30
完成耗时:3010

/**
 *优化使用withContext实现同步
 */
fun main() = runBlocking{
  val time1 = measureTimeMillis {
      //使用withContext实现同步
       val value1 = withContext(Dispatchers.Default) { fun1() }
       val value2 = withContext(Dispatchers.Default) { fun2() }
       println("num = ${value1 + value2}") 
   }
   println("完成耗时:$time")
}
挂起函数1
挂起函数2
num = 30
完成耗时:3034

惰性启动async

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

注意如果设置async(start = CoroutineStart.LAZY)并且通过await方式启动,将会导致挂起函数同步执行,直到await启动的协程程执行结束。这并不是惰性的预期用例。

fun main() = runBlocking{
  //指定async的start为CoroutineStart.LAZY
  val deferred1 = async(start = CoroutineStart.LAZY) { fun1() }
  val deferred2 = async(start = CoroutineStart.LAZY) { fun2() }
  
  //通过start方式启动,然后使用await获取结果
  deferred1.start()
  deferred2.start()
  val num1 = deferred1.await()  
  val num2 = deferred2.await()
  println("num = ${num1 + num2}")
  //运行结果
  挂起函数2
  挂起函数1
  num = 30
  
  //通过await方式直接启动并获取结果(注意:将会导致挂起函数同步执行)
  val num1 = deferred1.await()  
  val num2 = deferred2.await()
  println("num = ${num1 + num2}")
  //运行结果
  挂起函数1
  挂起函数2
  num = 30
}

async风格的函数

上面代码我们通过async{}代码块来执行挂起函数,达到并发目的。与此同时,我们还可以定义异步风格的函数来异步调用挂起函数,并使用async协程构建器并带有一个GlobalScope引用。我们给这样的函数名加上...Async后缀来突出表名异步:事实上该函数只做异步计算,并且需要使用延期的值来获得结果。(在kotlin的协程中强烈不推荐

注意这些...Async函数不是挂起函数,它们可以在任何地方使用。然而它们总是在调用它们的代码中意味着异步(并发)

 //async异步风格函数,返回一个Deferred类型
fun fun1Async() = GlobalScope.async {
    fun1()
}

fun fun2Async() = GlobalScope.async {
    fun2()
}

//测试:注意该函数是个普通函数右边没有runBlocking
fun main4() {
    //可以在协程外面调用...Async函数启动异步执行
    val deferred1 = fun1Async()
    val deferred2 = fun2Async()
    //但是等待结果必须放在协程内(其他挂起函数或者阻塞)
    runBlocking {
      val time = measureTimeMillis {
           val num1 = deferred1.await()
           val num2 = deferred2.await()
         	 println("num = ${num1 + num2}")
       }
      print("完成耗时:$time")
    }
}

//运行结果
挂起函数2
挂起函数1
num = 30
完成耗时:2006

协程调度器与线程

协程调度器

所有协程构建器诸如launchasync接收一个可选的CoroutineContext参数,它可以被用来显示的为一个新协程或其上下文元素指定一个调度器

Kotlin中协程调度器有如下几种

  • Dispatchers.Default:协程将会获取默认调度器,使用共享的后台线程池
  • Dispatchers.IO:同Default也是使用共享线程池,区别是IO只定义在JVM平台,Default其他平台也有如js
  • Dispatchers.Unconfined:不局限于任何特定的线程,即在遇到第一个挂起函数前代码运行在原线程中,在执行挂起函数后,运行在子线程中。
  • Dispatchers.Main:协程将运行在UI的主线程中(适用有UI的应用程序Android、JavaFx、Swing)
  • newSingleThreadContext:将使它获得一个新的线程, 在真实的应用程序中两者都必须被释放,当不再需要的时候,使用 close 函数,或存储在一个顶级变量中使它在整个应用程序中被重用
fun main() =runBlocking {
 
   //没有指定CoroutineContext,运行再父协程的上下问中,即runBlocking
	 launch {
      println("main runBlocking working in thread :${Thread.currentThread().name}")
   }

   //指定CoroutineContext为Unconfined不受限的,将工作再主线程中
   launch(Dispatchers.Unconfined) {
      println("Unconfined working in thread :${Thread.currentThread().name}")
   }

   //指定CoroutineContext为Default,将会获取默认调度器
   launch(Dispatchers.Default) {
      println("Default working in thread :${Thread.currentThread().name}")
   }

   //指定CoroutineContext为IO为阻塞任务而设计将,这是工作在共享线程池,
   launch(Dispatchers.IO) {
      println("IO working in thread :${Thread.currentThread().name}")
   }

   //指定为Main,将工作在UI主线程中(如:Android、JavaFx、Swing)
   launch(Dispatchers.Main) {
      println("Main working in thread :${Thread.currentThread().name}")
   }
   
   //指定为newSingleThreadContext,将工作在一个新指定线程中
   launch(customThread){
      println("newSingleThreadContext working in thread :${Thread.currentThread().name}")
   }
}

//运行结果
main runBlocking working in thread :DefaultDispatcher-worker-1
Unconfined working in thread :DefaultDispatcher-worker-2
Default working in thread :DefaultDispatcher-worker-1
IO working in thread :DefaultDispatcher-worker-1
newSingleThreadContext working in thread :MyThread
Main working in thread :main

注意:当调用launch{...}时不传参,它从启动它的CoroutineScope中承袭了上下文(以及调度器)。在这个案例中,它从main线程中的runBlocking主协程承袭了上下文。

在不同的线程间切换(withContext)

协程虽然是微线程,但是并不会和某一个特定的线程绑定,它可以在A线程中执行,并经过某一个时刻的挂起(suspend),等下次调度到恢复执行的时候,很可能会在B线程中执行。

withContext

  1. launchasyncrunBlocking 类似 withContext 也属于 Coroutine builders协程构造器。不过与他们不同的是,其他几个都是创建一个新的协程,而 withContext 不会创建新的协程。withContext 允许更改协程的执行线程,withContext 在使用时需要传递一个 CoroutineContext 。

  2. withContext 可以有返回值,这一点类似 async。async 创建的协程通过 await() 方法将值返回。而 withContext 可以直接返回。

    fun main2(){
            //创建一个协程,指定运行在Mian线程中
            CoroutineScope(Dispatchers.Main).launch{
                //通过withContext改变协程执行线程为IO
                val task1 = withContext(Dispatchers.IO){
                    delay(2000L)
                    println("模拟耗时网络请求在指定在IO线程中,然后将结果返回给task1")
                    "我是返回的数据"
                }
                println("返回到Mian线程中,获取到task1执行的结果并显示:$task1")
            }
        }
    

withContext 与 async 都可以返回耗时任务的执行结果。 一般来说,多个 withContext 任务是串行(同步)的, 且withContext 可直接返回耗时任务的结果。 多个 async 任务是并行(异步)的,async 返回的是一个Deferred<T>,需要调用其await()方法获取结果

协程异步流(Flow)

使用Flow特点

  • flowFlow类型构建器函数
  • flow{...}构建块中的代码可以挂起
  • flow构建器声明的函数不再需要使用suspend修饰符
  • 使用emit函数发射值
  • 使用collect函数收集值

基本使用

fun main() = runBlocking{
  //协程内调用Flow构建器函数的collect方法来获取值,间隔1s打印数值
  initData().collect{value->
    println(value)
  }
}

//通过flow构建器创建挂起函数,该函数不需要使用suspend修饰符
fun initData():Flow<Int> = flow{  
  repeat(5){i->
    delay(1000)
    emit(i)  //发射值
  }
}

//运行结果(间隔一秒)
0
1
2
3
4

流构建器

  • flow{}是最基础的一个流构建器

  • flowOf(data)用于构建发射固定值集的流

  • .asFlow()扩展函数,可将各种集合及序列转换为流

//基础流构建器——flow{...}
fun flow1():Flow<Int> = flow{
    for(i in 1..5){emit(i)}
}

//固定值流构建器——flowOf()
fun flow2():Flow<String> = flowOf("Judy","Susan","Tom")

//将集合/数组/可序列对象转换为流——asFlow()
val list:List<Int> = listOf(1,2,3,4)            //集合
val array:Array<Any> = arrayOf("Judy",false,1)  //数组
val interval  =(1..5)                           //区间
fun flow3():Flow<Int> = list.asFlow()
fun flow4():Flow<Any> = array.asFlow()
fun flow5():Flow<Int> = interval.asFlow()

fun testFlow() = runBlocking{
   print("flow{}——>")
   flow1().collect { value -> print("$value,") }
  
   print("\nflowOf()——>")
   flow2().collect { value -> print("$value,") }
  
   print("\nlist.fasFow{}——>")
   flow3().collect { value -> print("$value,") }
  
   print("\narray.fasFow{}——>")
   flow4().collect { value -> print("$value,") }
}

//运行结果
flow{}——>1,2,3,4,5,
flowOf()——>Judy,Susan,Tom,
list.fasFow{}——>1,2,3,4,
array.fasFow{}——>Judy,false,1,
interval.fasFow{}——>1,2,3,4,5,

更改流发射上下文

流构建器flow{}代码运行在相应流收集器(collect)提供的上下文中,不允许从其他上下文中发射(emit)

  • (错误方式)使用withContext更改流发射的上下文

    //通过flow流构建器构建一个流
    private fun testFlow2():Flow<Int> = flow{
            //使用withContext切换指定上下文(错误方式!!!)
            withContext(Dispatchers.Default){
                repeat(3){
                    delay(1000L)
                    emit(it)
                }
            }
        }
    
    fun main() = runBlocking{
      testFlow2().collect{value->println(value)}
    }
    
    //运行结果(在flow{}代码块内,不允许从其他上下文发射)
    Exception in thread "main" java.lang.IllegalStateException: Flow invariant is violated:
    
  • (正确方式)使用flowOn操作符更改流发射的上下文

    //通过在flow{}流构建器尾部使用flowOn(CoroutineContext)来更改流发射的上下文
    private fun testFlow3(): Flow<Int> = flow {
            repeat(3) {
                delay(1000L)
                emit(it)
            }
    }.flowOn(Dispatchers.Default)
    
    //运行结果
    0
    1
    2
    

流操作符

  • transform:流转换操作符,可用来模仿简单转换(如mapfilter)和实施更复杂的转换。使用transform操作符,我们可以发射(emit)任意值任意次

    fun main() = runBlocking{
      (1..3).asFlow() 												  //一个经区间转换后的流
            .transform{request->                //使用transform操作符对流进行转换
               emit("Making request $request")  //第一次发射
               emit(performRequest(request))    //第二次发射  
             }
             .collect{response->println(response)}
    }
    //挂起函数,对传入参数进行转换,内部延迟1s
    suspend fun performRequest(request:Int):String{
      delay(1000)
      return "response $request"
    }
    
    //运行结果
    Making request 1
    response 1
    Making request 2
    response 2
    Making request 3
    response 3
    
  • 末端操作符(collecttoListtoSetfirstreduce)

    • collect:是最基本的末端操作赋,收集emit发射的值
    • toList:将流转换为List集合(元素可重复)
    • toSet:将流转换为set集合(元素不可重复)
    • first:获取流中第一个值
    • reduce:累加函数,第一个参数是用来叠加的返回值,第二个参数是本次循环中列表的值
    val interval = (1..5)
    
    fun main() = funBlocking{
      //collect操作符,收集发射的值
      interval.asFlow().collect{value->print("$value,")}  //1,2,3,4,5
      
      //toList操作符,将流转换为List集合
      val list = interval.asFlow().toList()
      println(list)   //[1,2,3,4,5]
      
      //toSet操作符,将流转换为Set集合
      val array:Array<Int> = arrayOf(1,2,3,3,4,4,5)
      val setData = array.asFlow().toSet()
      println(setdata)   //[1,2,3,4,5]
      
      //first操作符,获取流第一个值
      val value = interval.asFlow().first()
      println(value)     //1
      
      //reduce操作符,对流元素进行累加
      val sum = interval.asFlow().reduce{sum,i->sum+i}   
      println(sum)       //25
      //实际测试reduce不仅可以累加甚至可以任何操作,第一个参数为返回的结果值,第二个参数为本次循环中列表值
      val response = interval.asFlow().reduce(result:Any,i->"response = $i")
       println(response)      //response = 5 
    }
    
  • take:限长操作符,在流触及限制长度的时候会将它的执行取消(只会取限制内的流数据)

    fun initFlowData():Flow<Int> = flow{
      for(i in 1..5){
        delay(1000L)
        emit(i)
      }
    }
    
    fun main() = runBlocking{
      initFlowData()
      				.take(2)   //限长操作符,只取前两个
              .collect{value->println(value)}
    }
    
    //运行结果
    1
    2
    
  • 展平操作符

    • map:遍历每一个元素

      fun main() = runBlocking {
         val list1 = listOf<Int>(1, 2, 3)
         val response = list1.map {
              it * 2    //遍历List,将每个元素乘以2换成的结果放到List中
         }
         println("response = $response")
      }
      //运行结果
      response = [2, 4, 6]
      
    • flatmap:遍历每一个元素,并铺平元素

      先来看看嵌套集合使用map和flatMap转换后的区别

      //初始化集合
      val list  = listOf(listOf(1,2,3),listOf("Judy","SuSan","Tom"))
      

      使用Map操作

      val responseMap = list.map { value ->
                  value   //遍历集合每个元素,不做处理直接返回
      }
      println("responseMap = $responseMap")
      //运行结果(内嵌集合不会铺平)
      responseMap = [[1, 2, 3], [Judy, SuSan, Tom]]
      

      使用flatmap操作

      val responseFlatMap = list.flatMap { value ->value }
      //运行结果,可见flatMap将内嵌集合元素全部铺平在一个List中返回
      //[1, 2, 3, Judy, SuSan, Tom]
      

      以上list.flatMap{value->value}可以被简化为list.flatten(),返回给定集合中所有(内嵌)集合的所有元素的单个列表(铺平)。

      //返回给定集合中所有(内嵌)集合的所有元素的单个列表(铺平)。
      val responseFlatMap = list.flatten()
      //[1, 2, 3, Judy, SuSan, Tom]
      

      或者通过value.asIterable()返回一个迭代器

      val responseFlatMap = list.flatMap{value->value.asIterable()}
      //[1, 2, 3, Judy, SuSan, Tom]
      
    • flatMapConcat{}:接受一个内部流(flow{}构建器创建的挂起函数),等待内部流完成之后再开始收集下一个值

      val list = listOf(1,2,3)
      list.asFlow().flatMapConcat{value->
                      //内部流
        							concatFlowMap(value)
      							}
                   .collect{value->
                       println(value)
                   }
      
      //通过flow{}流构建器,创建一个挂起函数
      fun concatFlowMap(value:Int):Flow<String> = flow{
          emit("delay before $value")
          delay(1000L)     //延迟1s
          emit("delay adter $value")
      }
      
      //运行结果(每次遍历等待内部流执行结束才会收集下一个值)
      delay before 1
      delay after 1
      delay before 2
      delay after 2
      delay before 3
      delay after 3
      
    • flatMapMerge{}:接受一个内部流(flow{}构建器创建的挂起函数),无需等待内部流执行完成,并发收集流发射的值

      val list = listOf(1,2,3)
      list.asFlow().flatMapMerge{value->
                      //内部流
        							mergeFlowMap(value)
      							}
                   .collect{value->
                       println(value)
                   }
      
      //通过flow{}流构建器,创建一个挂起函数
      fun mergeFlowMap(value:Int):Flow<String> = flow{
          emit("delay before $value")
          delay(1000L)     //延迟1s
          emit("delay adter $value")
      }
      
      //运行结果(遍历不会等待内部流执行结束,并发收集所有给定元素)
      delay before 1
      delay before 2
      delay before 3
      
      delay after 1
      delay after 2
      delay after 3
      
    • flatMapLatest:接受一个内部流(flow{}构建器创建的挂起函数),并发收集且发出新流后立即取消先前流的收集

      val list = listOf(1,2,3)
      list.asFlow().flatMapMerge{value->
                      //内部流
        							mergeFlowMap(value)
      							}
                   .collect{value->
                       println(value)
                   }
      
      //通过flow{}流构建器,创建一个挂起函数
      fun latestFlowMap(value:Int):Flow<String> = flow{
          emit("delay before $value")
          delay(1000L)     //延迟1s
          emit("delay adter $value")
      }
      
      /**
       *运行结果(并发收集流值,当新流(delay before 2)到来,还在等待收集(delay after 1)的流
       *被取消了
       */
      delay before 1
      delay before 2   //取消了delay after 1流的收集
      delay before 3   //取消了delay after 2流的收集
      delay adter 3    //因为后面没有新流到来,因此延迟1s后收集到该流
      
  • 流组合操作符

    • Zip:用于组合两个流中的相关值(注意:会按照最小长度流来组合)

      val list1 = listOf(1,2,3)
      val list2 = listOf("Judy","Susan","Tom","Peter")
      list1.asFlow().zip(list2.asFlow()){list1Value,list2Value->
                        "$list1Value——>$list2Value"   
      						  }.collect{value->
        								println(value)
      							}
      
      //运行结果(会按最小长度流进行组合,超过的流元素被忽略,这里忽略了list2.sFlow流中的'Peter')
      1——>Judy
      2——>Susan
      3——>Tom
      

扩展

可在全局创建的协程: lauch 与 runBlocking

lauch 与 runBlocking都能在全局开启一个协程,但lauch 是非阻塞的 而runBlocking 是阻塞的

  • launch创建协程

    btn.setOnClickListener {
        CoroutineScope(Dispatchers.Main).launch{
            delay(500)     //延时500ms
            Log.e("TAG","1[当前线程为:${Thread.currentThread().name}]")
        }
        Log.e("TAG","2[当前线程为:${Thread.currentThread().name}]")
    }
    //运行结果
    2[当前线程为:main]
    1[当前线程为:main]
    

    从上面运行结果可以看出,通过CoroutineScope.launch开启一个协程,协程体里的任务就会先挂起(suspend),不阻塞线程继续执行后续的代码,让CoroutineScope.launch后面的代码继续执行,直到协程体内的方法执行完成再自动切回来所在的上下文。

  • runBlocking创建协程

    btn.setOnClickListener {
        runBlocking{
            delay(500)    //延时500ms
            Log.e("TAG","1[当前线程为:${Thread.currentThread().name}]")
        }
        Log.e("TAG","2[当前线程为:${Thread.currentThread().name}]")
    }
    //运行结果
    1[当前线程为:main]
    2[当前线程为:main]
    

    runBlocking里的任务如果是非常耗时的操作时,会一直阻塞当前线程,在实际开发中很少会用到runBlocking。由于runBlocking 接收的 lambda 代表着一个 CoroutineScope,所以 runBlocking 协程体内可继续通过launch来继续创建一个协程,避免了lauch所在的线程已经运行结束而切不回来的情况。

可返回结果的协程:withContext 与 async

withContext 与 async 都可以返回耗时任务的执行结果。 一般来说,多个withContext 任务是串行的, 且withContext 可直接返回耗时任务的结果。 多个 async 任务是并行的,async 返回的是一个Deferred,需要调用其await()方法获取结果

  • withContext获取耗时任务结果

    btn.setOnClickListener {
        CoroutineScope(Dispatchers.Main).launch {
            val time1 = System.currentTimeMillis()
     
            val task1 = withContext(Dispatchers.IO) {
                delay(2000)
                Log.e("TAG", "1.执行task1.... [当前线程为:${Thread.currentThread().name}]")
                "one"  //返回结果赋值给task1
            }
                    
            val task2 = withContext(Dispatchers.IO) {
                delay(1000)
                Log.e("TAG", "2.执行task2.... [当前线程为:${Thread.currentThread().name}]")
                "two"  //返回结果赋值给task2
            }
     
            Log.e("TAG", "task1 = $task1  , task2 = $task2 , 耗时 ${System.currentTimeMillis()-time1} ms  [当前线程为:${Thread.currentThread().name}]")
        }
    }
    //运行结果
    1.执行task1.... [当前线程为:DefaultDispatcher-worker-1]
    2.执行task2.... [当前线程为:DefaultDispatcher-worker-1]
    task1 = one  , task2 = two , 耗时 3009 ms  [当前线程为:main]
    

    可见,多个withConext是串行执行,如上代码执行顺序为先执行task1再执行task2。这是因为withConext是个 suspend 函数,当运行到 withConext 时所在的协程就会挂起,直到withConext执行完成后再执行下面的方法。所以withConext可以用在一个请求结果依赖另一个请求结果的这种情况。

挂起函数
  • 挂起函数,挂起所在协程

    在一个协程内,挂起函数会保证挂起点后面的代码只能在挂起函数执行完后才能执行(async异步除外),所以挂起函数保证了协程内的执行顺序。但是这里并没有阻塞线程

    代码实例

    fun main(){
        //创建一个协程
        GlobalScope.launch(Dispatcher.Unconfined) {
           fun1() //协程内调用挂起函数,该函数会挂起所在的协程,直到挂起函数执行完毕后才会继续执行后续fun2
           fun2()
        }
    }
    
    //挂起函数1
    suspend fun fun1(){
      delay(2000L)
      println("fun1")
    }
    
    //挂起函数2
    suspend fun fun2(){
      delay(1000)
      pintln("fun2")
    }
    
  • 挂起函数不会阻塞线程

    挂起函数挂起协程,并不会阻塞协程所在的线程,例如协程的delay()挂起函数会暂停协程一定时间,并不会阻塞协程所在线程,但是Thread.sleep()函数会阻塞线程。

    fun main(args: Array<String>) {
        // 创建一个单线程的协程调度器,下面两个协程都运行在这同一线程上
        val coroutineDispatcher = newSingleThreadContext("ctx")
        // 启动协程 1
        GlobalScope.launch(coroutineDispatcher) {
            println("the first coroutine")
            delay(200)
            println("the first coroutine")
        }
        // 启动协程 2
        GlobalScope.launch(coroutineDispatcher) {
            println("the second coroutine")
            delay(100)
            println("the second coroutine")
        }
        // 保证 main 线程存活,确保上面两个协程运行完成
        Thread.sleep(500)
    }
    //运行结果
    the first coroutine
    the second coroutine
    the second coroutine
    the first coroutine
    

    从上面结果可以看出,同一个线程内创建两个协程,当协程 1 暂停 200 ms 时,线程并没有阻塞,而是执行协程 2 的代码,然后在 200 ms 时间到后,继续执行协程 1 的逻辑。所以挂起函数并不会阻塞线程,这样可以节省线程资源,协程挂起时,线程可以继续执行其他逻辑。

Kotlin中获取Class类型对象

在Java中我们获取Class类型对象常用两种方法:对象获取和类获取

class Person{}

//对象方式获取Class类型对象
Person person = new Person();
Class class1 = person.getClass();

//类方式获取Class类型对象
Class class2 = Person.class

在kotlin中我们获取Class类型对象方式有

class Person{}

//对象方式获取Class类型对象
val person = Person()
val class1 = person.java.class    //javaclass
val class2 = person::class.java.  //javaclass

//类获取
val class3 = Person::class        //kClass

Android提供了Kotlin协程使用方式

在Activity/Fragment通过继承 CoroutineScope by MainScope()来在Activity/Fragment中使用协程,并在onDestroy中调用协程的cancel()方法取消当前当前协程作用域内所有子协程操作。

在搭建架构时我们可以在BaseActivity中使用如下:

//Activity基类继承CoroutineScope的代理类MainScope(),其自身和子类的内部可使用协程
open abstract class BaseActivity<T : IPresenter> : AppCompatActivity(), CoroutineScope by MainScope() {

    lateinit var mPresenter: T
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(setLayout())
        initView()
    }
    abstract fun setLayout(): Int
    abstract fun initView()

    override fun onDestroy() {
        super.onDestroy()
        //取消当前Activity协程作用域内的所有子协程
        cancel()
        mPresenter.detachView()
    }
}

在其子类Activity中可直接使用协程

class LoginActivity:BaseActivity<ILoginContract.AbsLoginPresenter>(),ILoginContractor.ILoginView,{

override fun setLayout(): Int {
        return R.layout.activity_login
    }

override fun initView() {
        ...
        //通过launch创建一个协程
        launch{
          //todo something...
        }
    }
}

当LoginActivity销毁的时候,回调用父类的onDestory()方法,取消LoginActiviy中作用域内的左右协程操作。

当然,我们通常不在View中直接操作Mode,而是通过Presenter来操作Mode,那么我们可以将View层的协程传递给Presenter中使用,使Presenter中的协程也受到View生命周期的约束

//LoginPresenter实现类(将父类的协程代理类scope传入,并让其继承)
class LoginPresenterImpl(mView: ILoginContractor.ILoginView, scope: CoroutineScope) :
    ILoginContractor.AbsLoginPresenter(mView), CoroutineScope by scope {
    override fun login() {
        //启动协程
        launch {
            mView!!.showDialog()
            try {
                //耗时操作切换协程运行至IO线程中
                var result = withContext(Dispatchers.IO) {              				    				                                 NetWorkUtil.creatorApiService(ITestApiServer::class.java, false, null).testMehod2()
                }
                if (result!!.code == 200) {
                    mView!!.success(result.data)
                } else {
                    mView!!.failed(result.message)
                }
            } catch (e: Exception) {
                e.printStackTrace()
            }
            mView!!.closeDialog()
        }
    }    
}

其他的类:

ILoginContractor:用于统一管理V、M

interface ILoginContractor {
    interface ILoginView : BaseView {
        open fun showDialog()
        fun closeDialog()
        fun success(value: UserInfo2)
        fun failed(errorMessage: String)
    }

    abstract class AbsLoginPresenter(mView: ILoginView) : BasePresenter<ILoginView>(mView) {
        abstract fun login()
    }
}

BasePresenter

/**
 *BasePrenser管理当前Presenter内的View实例
 */
open class BasePresenter<T:BaseView>(mView: T) : IPresenter {
  
    private var mMvpView: WeakReference<T>? = WeakReference(mView)

    var mView = let {
        try {
            mMvpView!!.get()
        } catch (e: Exception) {
            throw Exception(
                "Please call Presenter.attachView(MvpView) before\" +\n" +
                        "                    \" requesting data to the Presenter"
            )
        }
    }

    override fun detachView() {
        mMvpView?.clear()
        mMvpView = null
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值