本文已参与「新人创作礼」活动,一起开启掘金创作之路。
通过前面的学习,我们知道,当需要执行异步任务并获取其结果时,可以通过开启一个协程来实现。那么,当我们需要执行多个异步任务并获得每个异步任务的结果时,我们应该怎么做呢?
没错,我们可以通过开启多个协程来实现。但更好的方式是使用 Flow 来实现。Flow 指的是异步流,它可以很方便地获取多个异步任务的结果。
一、Flow 的创建
Flow 有三种构建器:
- flow:通用流构建器。
- flowOf:用于发射固定值。
- asFlow:这是一个扩展函数,用来将集合或序列转换为流。
flow 是一个构造函数,用来生成一个 Flow 对象。在 Flow 中,使用 emit() 可以发射一个值,使用 collect() 可以收集发射的值。
举个简单使用 Flow 的例子,构造一个每隔一秒发射一个 Int 值的 Flow,并用 collect() 收集发射的值:
fun main() {
runBlocking {
flow {
repeat(3) {
delay(1000)
emit(it)
}
}.collect {
println(it)
}
}
}
运行程序,输出如下:
0
1
2
Flow 被构建出来后,可以多次执行,获得相同的数据:
fun main() {
runBlocking {
val flow = simpleFlow()
println("collect")
flow.collect { println(it) }
println("collect again")
flow.collect { println(it) }
}
}
fun simpleFlow() = flow<Int> {
println("Flow started")
repeat(3) {
delay(1000)
emit(it)
}
}
运行程序,输出如下:
collect
Flow started
0
1
2
collect again
Flow started
0
1
2
从输出中我们可以看出,Flow 在构建时并没有立即执行,是在收集后才开始执行的。
这说明 Flow 是一种类似于序列的冷流,冷流和热流的区别是:
- 冷流中的代码直到流被收集的时候才会执行。
- 热流中的代码会立即执行,不管有没有被收集。
可以看出,冷流与热流的区别和懒汉式与饿汉式的区别很类似,冷流只会在需要时才开始运行,而热流会立即运行。在 Kotlin 中,与 Flow 对应的热流是 Channel,关于 Channel 的内容我们将在之后的文章中学习,本文暂且不提。
二、Flow 的生命周期
一般来讲,Flow 会经过 onStart()、collect()、onCompletion() 三个生命周期:
注:严格来说,collect() 并不属于生命周期,笔者这么讲只是为了便于理解。
fun main() {
runBlocking {
flow {
for (i in 1..5) {
delay(100)
emit(i)
}
}.onStart {
println("onStart")
}.onCompletion {
println("onCompletion")
}.collect {
println(it)
}
}
}
运行程序,输出如下:
onStart
1
2
3
4
5
onCompletion
在发生异常时,onComplete() 中可以收到异常,也可以通过 catch() 操作捕捉到异常信息:
fun main() {
runBlocking {
flow {
for (i in 1..5) {
delay(100)
emit(i)
if (i == 3) throw Exception("my error")
}
}.onStart {
println("onStart")
}.onCompletion { exception ->
println("onCompletion: $exception")
}.catch { exception ->
println("catch: $exception")
}.collect {
println(it)
}
}
}
运行程序,输出如下:
onStart
1
2
3
onCompletion: java.lang.Exception: my error
catch: java.lang.Exception: my error
需要注意的是,catch() 和 onCompletion() 一起使用时,两者的顺序会影响运行结果:
- 如果在 onCompletion() 之前调用 catch(),只有 catch() 中能收到异常信息,onCompletion() 中将收不到异常信息。
- 如果在 onCompletion() 之后调用 catch(),onCompletion() 和 catch() 中都可以收到异常信息。
顺序之所以会影响结果,是因为流具有连续性:流的每次单独收集都是按顺序执行的(除非使用特殊操作符),从上游到下游的每个过渡操作符都会处理每个发射出的值,然后再交给末端操作符。
onCompletion() 能收到异常。但它并不会捕获这个异常,而是处理完后把异常抛出去。所以在 onCompletion() 之后的 catch() 能够捕获到异常信息。
catch() 能收到异常,也会捕获异常。异常被捕获之后,就不会再往外抛了,所以如果 onCompletion() 写在 catch() 之后,将收不到异常信息。
三、Flow 上下文切换
默认情况下,流的收集和流的构建共用同一个协程上下文,流的这个属性称为上下文保存。
但这种应用场景较少,通常我们需要的是 Flow 在子线程中构建,在主线程中收集。比如网络请求、文件读写,都需要在子线程中执行任务,在主线程中更新任务执行状态。
这时我们需要用到 flowOn() 操作符更改上下文。flowOn() 会将其之前的代码块设置为指定的线程。
fun main() {
runBlocking {
flow {
println("start: ${Thread.currentThread().name}")
repeat(3) {
delay(1000)
this.emit(it)
}
println("end: ${Thread.currentThread().name}")
}.flowOn(Dispatchers.Default).collect {
println("collect: $it, ${Thread.currentThread().name}")
}
}
}
运行程序,输出如下:
start: DefaultDispatcher-worker-1
collect: 0, main
collect: 1, main
end: DefaultDispatcher-worker-1
collect: 2, main
可以看出,数据的发射和数据的收集所在的线程是不一样的。因此 end: DefaultDispatcher-worker-1 和 collect: 2, main 打印的先后顺序也不是固定的。
注:在 Flow 中不允许使用 withContext 切换上下文。
如果想要修改收集流的协程,可以通过 launchIn() 函数,使用 onEach() + launchIn() 替换 collect() 可以指定流的收集所在的协程。
例如:
viewModelScope.launch {
flow {
Log.d("~~~", "start: ${Thread.currentThread().name}")
repeat(3) {
delay(1000)
this.emit(it)
}
Log.d("~~~", "end: ${Thread.currentThread().name}")
}
.flowOn(Dispatchers.Main)
.onEach {
Log.d("~~~", "collect: $it, ${Thread.currentThread().name}")
}
.launchIn(CoroutineScope(Dispatchers.IO))
.join()
}
在这个例子中,我们将 Flow 的构建指定在主线程,收集指定在 IO 线程。运行程序,输出如下:
D/~~~: start: main
D/~~~: collect: 0, DefaultDispatcher-worker-1
D/~~~: collect: 1, DefaultDispatcher-worker-1
D/~~~: end: main
D/~~~: collect: 2, DefaultDispatcher-worker-1
launchIn() 函数的源码很简单:
public fun <T> Flow<T>.launchIn(scope: CoroutineScope): Job = scope.launch {
collect() // tail-call
}
可以看到,launchIn() 会返回一个 Job 对象,有了 Job 对象,我们还可以在收集的中途取消收集。
四、Flow 的取消
Flow 在使用 emit() 发射数据时,会通过 ensureActive() 检测当前 Flow 是否处于活跃状态,如果不在活跃状态,则会直接抛出 CancellationException 异常并取消整个 Flow。
举个例子:
runBlocking {
flow {
(1..5).forEach {
emit(it)
}
}.collect {
println(it)
if (it == 3) cancel()
}
}
运行程序,输出如下:
1
2
3
Exception in thread "main" kotlinx.coroutines.JobCancellationException: BlockingCoroutine was cancelled; job=BlockingCoroutine{Cancelled}@365185bd
可以看到,正是因为这个特性,使得 Flow 在执行 CPU 密集型任务时也能及时取消。
但出于对性能的考虑,大多数其他操作不会通过 ensureActive() 检测 Flow 的状态,需要开发者明确指定是否检测。
不能被取消的例子:
runBlocking {
(1..5).asFlow().collect {
println(it)
if (it == 3) cancel()
}
}
运行程序,输出如下:
1
2
3
4
5
Exception in thread "main" kotlinx.coroutines.JobCancellationException: BlockingCoroutine was cancelled; job=BlockingCoroutine{Cancelled}@77ec78b9
这里我们没有通过 emit() 发射数据,而是通过 asFlow() 将 Kotlin 中的 Range 对象转成流发射,在收集的途中调用 cancel() 函数尝试取消流。
可以看到 Flow 并没有被及时取消,因为这时候 Flow 在发射数据时不会通过 ensureActive() 检测 Flow 的状态。
想要解决这个问题,需要使用 cancellable() 函数,这个函数表示此 Flow 需要执行取消检测:
fun main() {
runBlocking {
flow {
(1..5).forEach {
emit(it)
}
}.cancellable().collect {
println(it)
if (it == 3) cancel()
}
}
}
运行程序,输出如下:
1
2
3
Exception in thread "main" kotlinx.coroutines.JobCancellationException: BlockingCoroutine was cancelled; job=BlockingCoroutine{Cancelled}@42607a4f
可以看到,Flow 被及时取消了。
五、小结
Flow 可以用来收集多个异步任务的结果,本文我们学习了 Flow 的基本用法,包括 Flow 的构建,Flow 的生命周期,Flow 的上下文切换,Flow 的取消。Flow 在实际开发中非常实用,后续我们还会继续介绍 Flow 的一些特性。