Kotlin协程

一.协程的基本用法

kotlin没有把协程纳入标准库中,需要单独导入协程的依赖包:

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'

开启一个协程最简单的方式:

GlobalScope.launch { 
    println("The first coroutine scope")
}

GlobalScope.launch方法可以创建一个协程的作用域,println("The first coroutine scope")就是运行在协程中的代码。但是GlobalScope创建的永远是顶层协程,他的生命周期是和当前应用进程相互绑定的,即使Activity或Fragment已经被销毁,协程仍然在执行直到执行完成或者手动取消,这种协程的创建方法是不提倡的,因为会很容易就造成内存泄漏问题。

所以kotlin为我们提供了两个生命周期可以管控的非常好用的协程:lifecycleScope和viewModelScope,需要额外引入依赖:

implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'//lifecycleScope
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'//viewModelScope
  • lifecycleScope在Activity、Fragment中使用,会绑定Activity或Fragment的生命周期
  • viewModelScope在ViewModel中使用,绑定ViewModel的生命周期,而ViewModel的生命周期绑定Activity或Fragment,所以一定程度上可以说这两个协程的生命周期是两两对应的

以lifecycleScope为例:

val job = lifecycleScope.launch {
    println("The first coroutine scope")
}
job.cancel()//lifecycleScope会跟随activity或fragment的生命周期进行销毁,所以一般是不需要手动cancel的,除非是特殊场景需要

二.协程作用域构造器

1.runBlocking

如上所说GlobalScope是一个顶层协程,他的生命周期只由进程管控,一旦进程被杀死而协程作用域中的任务尚未完成,那么这个顶层协程作用域就无法完成自己的任务:

fun main() {
    GlobalScope.launch {
        Log.e("tag", "111")
        delay(3000)//一旦进程在这3秒内被杀,"222"就无法被打印出来
        Log.e("tag", "222")
    }
    Log.e("tag", "333")
}

运行结果:

你可以发现GlobalScope下的协程作用域和当前线程在一起死之前是没有任何交集的,GlobalScope协程下的任务并不能阻塞当前所在线程的任务执行,于是就有了runBlocking来完成这个工作:

fun main() {
    runBlocking {
        Log.e("tag", "111")
        delay(3000)
        Log.e("tag", "222")
    }
    Log.e("tag", "333")
}//运行结果:
// E/tag: 111
// E/tag: 222
// E/tag: 333

runBlocking可以保证其协程作用域下的所有代码和子协程在没有全部执行完成之前会一直阻塞当前线程,正是由于这个原因runBlocking通常只在测试环境下调试数据使用,否则会因为阻塞线程而导致性能上的问题。

2.launch

fun main() {
    val start = System.currentTimeMillis()
    runBlocking {
        launch {
            delay(1000)
            Log.e("tag", "111")
        }
        launch {
            delay(1000)
            Log.e("tag", "222")
        }
        launch {
            delay(1000)
            Log.e("tag", "333")
        }
    }
    val end = System.currentTimeMillis()
    Log.e("tag", (end - start).toString())
}//运行结果:
// E/tag: 111
// E/tag: 222
// E/tag: 333
// E/tag: 1002

这里的launch和上面的GlobalScope.launch方法不同,launch方法必须要在协程作用域中才可以调用,并在当前协程作用域中创建子协程,所有子协程都会并发执行且不会阻塞当前所在的父协程。

3.coroutineScope

如果launch方法中的逻辑很复杂,就需要抽取公共的代码到一个单独的方法中,而我们在launch方法中的代码是拥有协程作用域的,但这个单独的方法是没有的,kotlin为我们提供了suspend关键字,它可以将任意方法声明为挂起方法,挂起方法之间是可以相互调用的:

Kotlin协程_第1张图片

运行之后的结果没有任何不同。但是suspend关键字只能用来声明一个挂起方法,无法给它提供协程作用域,比如现在在printD方法中是无法调用launch方法的,因为launch方法要求必须在协程作用域中才可以被调用,于是kotlin又提供了coroutineScope方法来解决这个问题。coroutineScope方法也是一个挂起函数,所以他可以在任何被suspend修饰的挂起方法中调用,而且他可以继承suspend对应的外部作用域来创建一个子作用域,正是因为这个特性就可以为suspend挂起方法创建协程作用域:

private suspend fun printD(s: String) = coroutineScope {
    launch {
        delay(1000)
        Log.e("tag", s)
    }
}

而且coroutineScope方法和runBlocking方法有些类似,coroutineScope方法可以保证其作用域内的所有逻辑执行完成之前会一直阻塞当前继承的外部协程,但不会影响当前线程,所以基于这一点coroutineScope并不会造成性能上的问题,而runBlocking会阻塞线程造成性能问题。所以在实际项目中coroutineScope是会被经常使用的。

4.CoroutineScope

和上面的coroutineScope不一样,coroutineScope是一个方法可以创建子协程作用域,而CoroutineScope是一个接口,GlobalScope、LifecycleScope、ViewModelScope都是他的实现类。前面说过GlobalScope是顶层协程,如果想要取消他就只能去手动调用cancel方法,所有创建的顶层协程都需要逐个调用,这样的代码根本就没法维护而且奇丑无比。如果你想避免上述的问题且不想和任何组件的生命周期扯上关系,那么就可以用CoroutineScope来管控协程作用域:

val job = Job()
val scope = CoroutineScope(job)
scope.launch {
    //1
}
scope.launch {
    //2
}
job.cancel()

会发现这里的CoroutineScope(job)并不是一个接口该有的用法,因为接口不允许有构造方法,注意这里的CoroutineScope()此时并不是接口而是CoroutineScope的扩展方法,他会通过包装协程上下文对象(Job)来返回一个ContextScope的实例,源码:

@Suppress("FunctionName")
public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
    ContextScope(if (context[Job] != null) context else context + Job())

ContextScope是CoroutineScope接口的最为普通的实现类,生命周期管控只依赖于实现的协程上下文对象,他所创建的所有协程都会被job管控,就相当于job关联了一个总的协程作用域,ContextScope创建的都是总协程作用域下面的子协程,一旦job执行了cancel,所有协程都会被销毁,这样就大大降低了管理成本:

internal class ContextScope(context: CoroutineContext) : CoroutineScope {
    override val coroutineContext: CoroutineContext = context
    // CoroutineScope is used intentionally for user-friendly representation
    override fun toString(): String = "CoroutineScope(coroutineContext=$coroutineContext)"
}

5.async

所有的协程作用域都可以被赋值为一个Job对象,他们可以通过这个Job对象分别管控自己的协程作用域:

val job = Job()
val scope = CoroutineScope(job)
val job1 = scope.launch {
    //1
}
val job2 = scope.launch {
    //2
}
job1.cancel()
job2.cancel()

如果我们需要当前协程作用域返回一个结果并且能拿到这个结果的话,Job就无法完成这个任务。这里就需要使用async方法,他会返回一个Deferred对象,当需要使用协程作用域返回的值时,调用Deferred对象的await方法即可:

runBlocking {
    val start = System.currentTimeMillis()
    val result1 = async {
        delay(1000)
        5 + 5 //将作用域中最后一行代码作为返回值返回
    }.await()
    val result2 = async {
        delay(1000)
        4 + 4
    }.await()
    val end = System.currentTimeMillis()
    Log.e("tag", (result1 + result2).toString())
    Log.e("tag", (end - start).toString())
}//log打印:
// E/tag: 18
// E/tag: 2003

从运行耗时可以看出这段代码的总耗时是两个子协程的耗时总和,这是因为await方法会在async代码块执行完成之前将当前协程阻塞住,第二个async代码块必须等第一个执行完才可以执行,这样就变成了串行执行,那如何实现并行呢?

runBlocking {
    val start = System.currentTimeMillis()
    val result1 = async {
        delay(1000)
        5 + 5 //将作用域中最后一行代码作为返回值返回
    }
    val result2 = async {
        delay(1000)
        4 + 4
    }
    Log.e("tag", (result1.await() + result2.await()).toString())
    val end = System.currentTimeMillis()
    Log.e("tag", (end - start).toString())
}//log打印:
// E/tag: 18
// E/tag: 1002

如图所示,我们在每次调用async方法之后不立即调用await方法,仅仅是在需要用到async方法返回值时再进行await方法调用,这样两个async协程作用域就是并行关系,可以看到总耗时是1002毫秒即一个async代码块的耗时。

6.withContext

withContext也是一个挂起方法,是async的简化写法:

val result1 = withContext(Dispatchers.Default) {
    delay(1000)
    5 + 5 //将作用域中最后一行代码作为返回值返回
}

val result1 = async {
    delay(1000)
    5 + 5 //将作用域中最后一行代码作为返回值返回
}.await()

这两段代码是一样的,所以可以看出withContext会阻塞当前协程,唯一不同的是withContext方法强制我们要指定一个线程参数,这个线程参数就是来指定当前协程必须在一个怎样的线程中运行。就比如说一个网络请求得协程任务必须开在子线程中执行,如果开在UI线程就会出问题。

这个参数有几种值可选择:

Dispatchers.Default:开启低并发的子线程。当执行的任务属于高计算密集度的任务时,开启过高的并发会影响执行速率,意思就是把能拿到的CPU资源都给这个高密集度的计算任务,不再并发执行其他的任务。

Dispatchers.IO:开启较高并发的子线程。当需要执行的任务数量较多且计算密集度低就应该使用这个线程参数,如果使用Dispatchers.Default就会导致大量子协程任务处于阻塞等待状态,无法做到高并发执行。

Dispatchers.Main:不开启子线程,在主线程中执行。纯kotlin程序中使用这种类型的线程参数会报错。

Dispatchers.Unconfined:直接在当前所在线程中执行

你可能感兴趣的:(android,kotlin,android)