Kotlin - 协程

本篇将介绍Android中的协程

Kotlin - 协程

异步编程 之一 协程

协程的基本介绍

协程就非常轻量级的线程,线程是由系统调度的。线程它在进行切换或造成阻塞的时候开销都很大,而协程它是有开发者来进行控制的,也是依赖于线程的,但是协程挂起的时候是不需要阻塞线程的,几乎是无代价的。所以协程也想用户状态下的线程,非常轻量级

协程还有一点就是当它挂起的时候,它是不会阻塞线程的。它的底层实现就是异步处理阻塞任务

挂起:挂起其实就是用 suspend来修饰一个函数。被suspend修饰的函数称为挂起函数

基本使用

第一步肯定是添加依赖

1
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1"

GlobalScope

Kotlin中GlobalScope 类提供了几个协程构造函数

  1. launch

    接收参数

    launch是CoroutineScope的扩展函数,接受3个参数,前面两个都是常规参数,最后一个是对象式函数。

    1
    2
    3
    4
    5
    public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
    ): Job

    这样的话就可以采用闭包的写法:() 里面写常规参数,{} 里面写函数式对象的实现

    1
    GlobalScope.launch(Dispatchers.Unconfined) {...}
    1. 第一个参数CoroutineContext

      网上有说可以理解为协程的上下文,但按功能来说,它就是协程的线程调度器,有4种线程模式,用来指定运行线程:

      • Dispatchers.Main

        使用这个调度器在Android主线程上运行一个协程,可以用来更新UI

      • Dispatchres.IO

        使用这个调度器可以在主线程之外执行磁盘或网络I/O,在线程池中执行

      • Dispatchres.Default

        使用这个调度器可以在主线程之执行CPU密集型的工作,在线程池中执行

      • Dispatchres.Unconfined

        直接在调用者的线程执行

    2. 第二个参数CoroutineStart

      这个就是启动模式,通过更改启动模式可以达到你需要它的时候,再调用启动(使用LAZY)

      • DEAFAULT

        默认的模式,立即执行协程体

      • ATOMIC

        立即执行协程体,但在开始运行之前无法取消

      • UNDISPATCHED

        立即在当前线程执行协程体,直到第一个 suspend 调用

      • LAZY

        只有在需要的情况下运行

    3. block

      这个就是闭包的方法体,用来定义协程内部需要执行的操作

    返回值
    1. Job

      协程构造函数的返回值,也可以把Job看成协程对象本身,协程的操作都在Job身上了

      • Job.start

        启动协程,但除了将协程设置为Lazy外,都不用手动启动

      • Job.join

        等待协程执行完毕

      • Job.cancel

        取消一个协程

      • Job.cancelAndJoin

        等协程执行完毕之后再取消

    使用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    ...
    priavte val TAG = "test"
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    Log.e(TAG, "主线程id:${mainLooper.thread.id}")
    val job = GlobalScope.launch {
    delay(5000)
    Log.e(TAG, "协程执行结束 -- 线程id:${Thread.currentThread().id}")
    }
    Log.e(TAG, "主线程执行结束")
    }
    ...
    //Job中的方法
    job.isActive
    job.isCancelled
    job.isCompleted
    job.cancel()
    jon.join()

    运行结果:

    img

    特性

    在上面的launch中,我添加了等待5秒,但主线程还是可以继续执行。也就可以看出,launch是不会阻断主线程的

  2. async

    asynclaunch的用法基本一致区别在于:async的返回值是Deferred。async可以支持并发,一般与await一同使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    ...
    private val TAG = "async"
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    GlobalScope.launch {
    val toDayTime = GlobalScope.async {
    getThisTime()
    }
    val yesTerDay = GlobalScope.async {
    getYesterdayTime()
    }
    val time = toDayTime.await() + yesTerDay.await()
    Log.e(TAG, "昨天时间 和 今天时间为 --> $time")
    }
    }

    private suspend fun getThisTime(): String {
    delay(3000)
    Log.e(TAG, "getThisTime: 这里已经执行完毕")
    return "3-"
    }

    private suspend fun getYesterdayTime(): String {
    delay(5000)
    Log.e(TAG, "getYesterdayTime: 这里也已经执行完毕" )
    return "7"
    }
    ...

    运行结果:

    image-20210705110146134

    这里也可以看出async是不阻塞线程的,也就是是getThisTime()和getYestertdayTime()是同时进行的.所以获取到time的时间是5s,而不是8s。

    launch 没有返回值,或者说返回只是 job ,能够知道任务的状态,却不能携带返回结果。async 有返回值,也就是返回的是 Deferred ,它是继承的 job ,所有job有的,它都有,还具备了job没有的携带数据回来的能力。
    所以, launch 可以用来运行不需要操作结果的协程(如文件删除,创建等), async 可以用来运行异步耗时任务并且需要返回值的任务(网络请求,数据库操作,文件读写等)。

  3. withContext

    withContext这个函数主要用来切换到指定的线程,并且再withContext闭包结束之后再切换回之前的线程

    1
    2
    3
    4
    5
    6
    coroutineScope.launch(Dispatchers.Main) {      //  在 UI 线程开始
    val image = withContext(Dispatchers.IO) { // 切换到 IO 线程,并在执行完成后切回 UI 线程
    getImage(imageId) // 将会运行在 IO 线程
    }
    avatarIv.setImageBitmap(image) // 回到 UI 线程更新 UI
    }

非GlobalScope的API

  1. runBlocking

    runBlocking与launch的功能基本一致,不同在于runBlocking是会阻塞线程的(和Thread.sleep()一样)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    ...
    private val TAG = "runBlocking"
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    runBlocking {
    val a = await()
    Log.e(TAG, "onCreate: --> $a")
    }
    repeat(8) {
    Log.e(TAG, "主线程执行$it")
    }
    }

    private suspend fun await(): String {
    delay(2000)
    return "等待时间已过期"
    }
    ...

    运行结果

    image-20210705115001967

协程体(挂起函数)

协程体是一个用suspend关键字修饰的一个函数类型,也就是挂起函数。注意:挂起函数只能在协程中或其他挂起函数中使用,而且它是顺序执行的,下面将演示一下顺序执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
...
private val TAG: String = "launch"
overrid fun onCreate(savedInstanceState: Bundle?){
...
GlobalScope.launch{
val name = getName()
val userInfo = getUserInfo(name)
Log.e(TAG,"userInfo --> ${userInfo}")
}
repeat(8){
Log.e(TAG,"在主线程执行 --> $it")
}
...
}
suspend fun getName(): String{
delay(2000)
return "Tom"
}
suspend fun getUserInfo(name: String): String{
delay(2000)
return "userInfo.Name --> $token"
}

...

运行结果:

image-20210705104310480

挂起与恢复

协程之外线程里肯定还有需要执行的代码,我们来看看前面的代码在挂起后何时才能恢复执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
suspend fun getToken(): String {
delay(1)
Log.d(TAG, "getToken 开始执行,时间: ${System.currentTimeMillis()}")
return "ask"
}

suspend fun getResponse(token: String): String {
delay(2)
Log.d(TAG, "getResponse 开始执行,时间: ${System.currentTimeMillis()}")
return "response"
}

fun setText(response: String) {
Log.d(TAG, "setText 执行,时间: ${System.currentTimeMillis()}")
}

GlobalScope.launch(Dispatchers.Unconfined) {
Log.d(TAG, "协程 开始执行,时间: ${System.currentTimeMillis()}")
val token = getToken()
val response = getResponse(token)
setText(response)
}

for (i in 1..10) {
Log.d(TAG, "主线程打印第$i 次,时间: ${System.currentTimeMillis()}")
}

运行结果:

img

协程挂起后,虽然延迟的时间到了,但是还得等到线程空闲时才能继续执行,这里要注意,协程可没有竞争 CPU 时间段,协程挂起后即便可以恢复执行了也不是马上就能恢复执行。简单来说,就是协程挂起之后想要恢复,必须得等到线程空闲了才可以继续,不然继续等

协程生命周期

这里可以将所有有生命周期的类继承CoroutineSocpe,这样就可以让全部协程跟着生命周期结束

  • 如在activity里面使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    MainActivity : AppCompatActivity(), CoroutineScope by MainScope(){

    ...

    override fun onDestroy(){
    super.onDestory()
    cancel()
    }
    }
  • 在其他UI逻辑类中使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class MainActivityFacede : CoroutineScope {
    private val job = Job()

    override val coroutineContext: CoroutineContext
    get() = Dispatchers.Main + job

    fun destroy() {
    job.cancel()
    }
    }

    以上代码都会在调用 destroy 的时候取消这个作用域中的协程运行