Kotlin协程小记

Kotlin协程是什么?

官方说协程可以被认为是轻量级线程,但是在我的使用体验下来,Kotlin协程更像是一个助手,协助我们更好地使用线程,它可以在不同的线程间灵活切换,让代码以我们想要的顺序去执行,最直观的感受就是我可以少写很多回调操作,优雅地处理异步任务。

引入协程

build.gradle文件中添加:

1
2
3
4
dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:latest'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:latest'
}

使用

  • runBlocking

runBlocking会阻塞协程所在线程,可以直接返回运行结果,举个栗子:

1
2
3
4
5
6
7
8
9
10
11
fun testRunBlocking() {
println("$now 测试开始 $currentThread")
val result = runBlocking {
println("$now 协程开始 $currentThread")
delay(1000)
println("$now 协程结束 $currentThread")
return@runBlocking "$now 协程返回 $currentThread"
}
println("$now 测试结束 $currentThread")
println(result)
}
1
2
3
4
5
09:16:16.016 测试开始 运行在线程1
09:16:16.016 协程开始 运行在线程1
09:16:17.017 协程结束 运行在线程1
09:16:17.017 测试结束 运行在线程1
09:16:17.017 协程返回 运行在线程1
  • withContext

withContext只能在协程块(这个后面再说)内使用,所以在外面用runBlocking包了一层。它会阻塞协程所在线程,可以直接返回运行结果,举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
fun testWithContext() {
println("$now 测试开始 $currentThread")
val result = runBlocking {
return@runBlocking withContext(Dispatchers.IO) {
println("$now 协程开始 $currentThread")
delay(1000)
println("$now 协程结束 $currentThread")
return@withContext "$now 协程返回 $currentThread"
}
}
println("$now 测试结束 $currentThread")
println(result)
}
1
2
3
4
5
09:34:39.039 测试开始 运行在线程1
09:34:39.039 协程开始 运行在线程17
09:34:40.040 协程结束 运行在线程17
09:34:40.040 测试结束 运行在线程1
09:34:40.040 协程返回 运行在线程17
  • launch

launch不会阻塞协程所在线程,不可以直接返回运行结果,但是会返回一个Job对象(这个后面再说),举个栗子:

1
2
3
4
5
6
7
8
9
fun testLaunch() {
println("$now 测试开始 $currentThread")
GlobalScope.launch {
println("$now 协程开始 $currentThread")
delay(1000)
println("$now 协程结束 $currentThread")
}
println("$now 测试结束 $currentThread")
}
1
2
3
4
09:45:39.039 测试开始 运行在线程1
09:45:39.039 测试结束 运行在线程1
09:45:39.039 协程开始 运行在线程16
09:45:40.040 协程结束 运行在线程16
  • async

async不会阻塞协程所在线程,可以返回运行结果,但是需要用await获取,await只能在协程块(这个后面再说)内使用,所以在外面用runBlocking包了一层,举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun testAsync() {
runBlocking {
println("$now 测试开始 $currentThread")
val result = GlobalScope.async {
println("$now 协程开始 $currentThread")
delay(1000)
println("$now 协程结束 $currentThread")
return@async "$now 协程返回 $currentThread"
}
println("$now 测试结束 $currentThread")
println(result.await())
println("$now 真的结束 $currentThread")
}
}
1
2
3
4
5
6
09:50:49.049 测试开始 运行在线程1
09:50:49.049 测试结束 运行在线程1
09:50:49.049 协程开始 运行在线程17
09:50:50.050 协程结束 运行在线程17
09:50:50.050 协程返回 运行在线程17
09:50:50.050 真的结束 运行在线程1

可以看到我们通过await能拿到运行结果,而且await会阻塞所在线程。

  • suspend

之前多次提及的协程块,就是runBlockinglaunch等方法里面的代码块,一般都是执行耗时操作,我们还可以把这些操作抽取出来作为一个方法,这样的方法需要用suspend关键字修饰。suspend方法里面的代码块也是协程块,而且suspend方法也只能在协程块内使用(套娃?),suspend的意义更多是标记这是一个耗时方法,协程专用,避免误用导致主线程阻塞。上面的代码还可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fun testAsync() {
runBlocking {
println("$now 测试开始 $currentThread")
val result = GlobalScope.async {
return@async doSth()
}
println("$now 测试结束 $currentThread")
println(result.await())
println("$now 真的结束 $currentThread")
}
}

suspend fun doSth(): String {
println("$now 协程开始 $currentThread")
delay(1000)
println("$now 协程结束 $currentThread")
return "$now 协程返回 $currentThread"
}
1
2
3
4
5
6
09:53:29.029 测试开始 运行在线程1
09:53:29.029 测试结束 运行在线程1
09:53:29.029 协程开始 运行在线程16
09:53:30.030 协程结束 运行在线程16
09:53:30.030 协程返回 运行在线程16
09:53:30.030 真的结束 运行在线程1

嗯,结果是一样的。

庐山真面目

了解了suspend后,我们再看看协程这些个方法的庐山真面目:

1
2
3
4
public fun <T> runBlocking(
context: CoroutineContext,
block: suspend CoroutineScope.() → T
): T
1
2
3
4
public suspend fun <T> withContext(
context: CoroutineContext,
block: suspend CoroutineScope.() → T
): T
1
2
3
4
5
public fun CoroutineScope.launch(
context: CoroutineContext,
start: CoroutineStart,
block: suspend CoroutineScope.() → Unit
): Job
1
2
3
4
5
public fun <T> CoroutineScope.async(
context: CoroutineContext,
start: CoroutineStart,
block: suspend CoroutineScope.() → T
): Deferred<T>
1
public abstract suspend fun await(): T
1
public suspend fun delay(timeMillis: Long): Unit

可以看到withContextawaitdelay都是suspend方法,也难怪必须要在协程块内才可以使用了。我们也能清楚地看到各方法的返回值,runBlockingwithContext是直接返回运行结果的,async返回一个Deferred对象,我们需要调用其await方法获取运行结果,launch则返回一个Job对象,这个有什么用呢?还有传入的CoroutineContextCoroutineStart又是什么玩意?且看下文。

  • CoroutineContext

直译就是协程上下文,我们可以传入线程调度器指定协程的线程调度,有四种类型:

  1. Dispatchers.Default

    协程块在子线程运行。

  2. Dispatchers.Main

    协程块在主线程运行,Android即是在UI线程运行。

  3. Dispatchers.Unconfined

    协程块在协程所在线程运行,但是如果遇到挂起操作,则会在子线程运行后面的代码。

  4. Dispatchers.IO

    协程块在子线程运行。

  • CoroutineStart

协程的启动模式,有四种类型:

  1. CoroutineStart.DEFAULT

    协程块立即运行。

  2. CoroutineStart.LAZY

    协程块在调用启动方法后才运行。

  3. CoroutineStart.ATOMIC

    协程块立即运行,且不可取消。

  4. CoroutineStart.UNDISPATCHED

    协程块立即运行,会忽略设置的线程调度器而在协程所在线程运行,但是如果遇到挂起操作,则会在线程调度器指定线程运行后面的代码。

  • Job

提供操作协程块运行的能力,Deferred是其子类,他们都有以下几个方法:

  1. start

    启动协程块所在协程,一般用于CoroutineStart.LAZY启动模式。

  2. cancel

    取消协程块运行,但不会立马生效,有一定延迟。

  3. join

    这是一个suspend方法,会阻塞协程所在线程,直到协程块运行完毕。

  4. cancelAndJoin

    先调用cancel,再调用join,会阻塞协程所在线程,直到协程块运行完毕。

  • 生命周期

我们上面用launchasync启动协程时,都是在GlobalScope这个全局的生命周期内,它就类似于Application会存活在整个应用运行期间。这会带来什么影响呢?那就是我们可能没有及时回收相关资源而造成内存泄漏。因此官方提供了LifecycleScopeViewModelScope这种非全局的生命周期。例如在AppCompatActivityFragment中,协程就会受到LifecycleScope生命周期的约束,当走到onDestroy这一步时,还在运行且可取消的协程会被取消并做好资源回收。但是我们非要使用GlobalScope呢?也不是不行,自行在合适的地方调用cancel取消协程,甚至可以直接调用GlobalScope.cancel取消其范围内所有可取消协程。LifecycleScopeViewModelScope同样可以直接调用cancel取消其范围内的所有可取消协程。

  • 设置超时

除了手动调用cancel取消协程,我们还可以用withTimeout设置超时自动取消,举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fun testWithTimeout() {
println("$now 测试开始 $currentThread")
GlobalScope.launch {
try {
withTimeout(10000) {
println("$now 协程开始 $currentThread")
repeat(10) {
println("$now $it $currentThread")
delay(3000)
}
println("$now 协程结束 $currentThread")
}
} catch (e: TimeoutCancellationException) {
println("$now 协程超时 $currentThread")
}
}
println("$now 测试结束 $currentThread")
}
1
2
3
4
5
6
7
8
17:07:26.026 测试开始 运行在线程1
17:07:26.026 测试结束 运行在线程1
17:07:26.026 协程开始 运行在线程17
17:07:26.026 0 运行在线程17
17:07:29.029 1 运行在线程17
17:07:32.032 2 运行在线程17
17:07:35.035 3 运行在线程17
17:07:36.036 协程超时 运行在线程17

对比withTimeout,还有一个withTimeoutOrNull方法,它们都会阻塞所在线程,若不超时可以正常返回结果。其不同之处在于withTimeout超时后会抛出超时错误,而withTimeoutOrNull直接返回null却不抛错误。

小结

使用协程后,对于耗时操作,我们不但可以优雅地切换到子线程去处理,还可以拿到运行结果优雅地回到之前线程使用,免去了回调的俗套,轻松拿捏多线程的执行顺序,转异步为同步,化腐朽为神奇。

作者

EIong

发布于

2022-06-13

更新于

2022-06-13

许可协议