Kotlin 协程

Posted by OOFTF Blog on August 18, 2021

coroutines

  • 官方描述:协程通过将复杂性放入库来简化异步编程。程序的逻辑可以在协程中顺序地表达,而底层库会为我们解决其异步性。该库可以将用户代码的相关部分包装为回调、订阅相关事件、在不同线程(甚至不同机器)上调度执行,而代码则保持如同顺序执行一样简单。
  • 协程很重要的一点就是当它挂起的时候,它不会阻塞其他线程。协程底层库也是异步处理阻塞任务,但是这些复杂的操作被底层库封装起来,协程代码的程序流是顺序的,不再需要一堆的回调函数,就像同步代码一样,也便于理解、调试和开发。它是可控的,线程的执行和结束是由操作系统调度的,而协程可以手动控制它的执行和结束。

用同步代码写出异步效果

引入 coroutines 包

last version

1
2
3
4
5
6
7
8
9
10
11
12
13
// 核心库

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

//如果要使用 ViewModelScope 需要添加

implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0'

//如果要使用 LifecycleScope,请使用 需要添加

implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.0' 

关键类和方法

  • GlobalScope:launch
  • runBlocking
  • launch:Job
    • job.isActive
    • job.isCancelled
    • job.isCompleted
    • job.cancel()
    • jon.join()
  • GlobalScope:async
  • Deferred.await()
  • withContext
  • suspendCoroutine 可以在协程中解决回调问题
启动模式(CoroutineStart ;GlobalScope:launch的第二个参数)
启动模式 作用
DEFAULT 默认的模式,立即执行协程体
LAZY 只有在需要的情况下运行
ATOMIC 立即执行协程体,但在开始运行之前无法取消
UNDISPATCHED 立即在当前线程执行协程体,直到第一个 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
25
26
27
28
29
30
31
32
33
    val asyncTask = GlobalScope.async {
            Log.e("coroutines", "EEE-${Thread.currentThread().name}")
            delay(5000)
            Log.e("coroutines", "FFF-${Thread.currentThread().name}")
        }
    fun mainThread() {
        Log.e("coroutines", "000-${Thread.currentThread().name}")
        GlobalScope.launch(Dispatchers.IO) {
           Log.e("coroutines", "AAA-${Thread.currentThread().name}")
            suspendFun()
            Log.e("coroutines", "CCC-${Thread.currentThread().name}")
            asyncTask.await()
            Log.e("coroutines", "GGG-${Thread.currentThread().name}")
        }
        Log.e("coroutines", "DDD-${Thread.currentThread().name}")
    }

    suspend fun suspendFun() {
        withContext(Dispatchers.Main) {
            Log.e("coroutines", "BBB-${Thread.currentThread().name}")
        }
    }
 /*
    运行结果:
    EEE-DefaultDispatcher-worker-1
    000-main
    DDD-main
    AAA-DefaultDispatcher-worker-1
    BBB-main
    CCC-DefaultDispatcher-worker-2
    FFF-DefaultDispatcher-worker-1
    GGG-DefaultDispatcher-worker-1
    */
  • 从 000 后面执行的是 DDD 得知launch并没有阻塞主线程执行,即使是改为GlobalScope.launch(Dispatchers.Main)得到的结果也是一样的;这个特性和 runBlocking 相反
  • 从AAA BBB CCC的执行顺序可知,BBB 虽然和 AAA CCC 不在同一个线程执行顺序却是按照 AAA BBB CCC 的代码顺序执行的,这就是所谓的挂起函数
  • AAA 和 CCC 虽然在代码中属于同一块代码,确是在不同的线程中执行的。
  • AAA 和 CCC 可能在同一线程执行也可能在不同线程执行

实现原理

Continuation.resumeWith ——> 生成类.invokeSuspend -> 状态判断

  • 线程
  • 状态机 机制

1.5 为什么将 GlobalScope 设置为易碎的(DelicateCoroutinesApi)

因为 GlobalScope 不能通过 scope 作用域自动取消任务,只能手动通过 job 取消任务,在 Activity 中使用 GlobalScope 时,GlobalScope 的生命周期会比 Activity 更加长

怎么将回调转成挂起函数

怎么将回调转成挂起函数

异常处理

1
2
3
lifecycleScope.launch(Dispatchers.Main) {
    throw IndexOutOfBoundsException()
}

上面情况程序会直接崩溃

1
2
3
4
5
6
val handler = CoroutineExceptionHandler { _, exception ->
    println("Caught $exception")
}
lifecycleScope.launch(handler) {
    throw IndexOutOfBoundsException()
}

上面代码,程序不会崩溃,异常信息通过 CoroutineExceptionHandler 输出。

默认线程

  • GlobalScope 默认 work 线程
  • lifecycleScope 默认 主线程