Kotlin协程的异步流Flow(八)

文章目录

    • 一、前言
    • 二、Flow的简单演示
    • 三、Flow的取消
    • 四、构建Flow
    • 五、过度流操作符
    • 六、转换操作符
    • 七、限长操作符
    • 八、末端流操作
    • 九、流是连续的
    • 十、Flow上下文
    • 十一、withContext 发出错误
    • 十二、flowOn 操作符
    • 十三、缓冲
    • 十四、合并
    • 十五、处理最新值
    • 十六、Zip
    • 十七、Combine
    • 十八、flatMapConcat 与 flattenConcat
    • 十九、flatMapMerge 与 flattenMerge
    • 二十、flatMapLatest
    • 二十一、Flow的异常
    • 二十二、异常的透明性
    • 二十三、完成情况
    • 二十四、collect和launchIn
    • 二十五、参考链接

一、前言

​ 挂起函数可以返回一个值,但是没有办法返回多个值。List可以返回多个值,但是它是一次性返回的。Sequence可以根据需要返回多个值,但它是阻塞的。所以可以使用Flow来对数据进行处理,而且Flow是数据流可以对数据进行各种复杂操作,并且简化异步操作。

二、Flow的简单演示

fun simple(): Flow<Int> = flow { // 流构建器
    for (i in 1..3) {
        delay(100) // 假装我们在这里做了一些有用的事情
        emit(i) // 发送下一个值
    }
}
@Test
fun flowSample(){
    runBlocking {
        // 启动并发的协程以验证主线程并未阻塞
        launch {
            for (k in 1..3) {
                println("I'm not blocked $k")
                delay(100)
            }
        }
        // 收集这个流
        simple().collect { value -> println(value) }
    }
}

运行结果如下:

I'm not blocked 1
1
I'm not blocked 2
2
I'm not blocked 3
3

这段代码在不阻塞主线程的情况下每等待 100 毫秒打印一个数字。如果换成List或者Sequence就会有不一样的结果。

这里面有以下注意点:

  • 名为 flow 的 Flow 类型构建器函数。
  • flow { ... } 构建块中的代码可以挂起。
  • 流使用 emit 函数 发射值。
  • 流使用 collect 函数收集值。

Flow是冷流,也就是说Flow是在数据被收集的时候才开始运行。以下是示例

fun simple(): Flow<Int> = flow { // 流构建器
        for (i in 1..3) {
            delay(100) // 假装我们在这里做了一些有用的事情
            emit(i) // 发送下一个值
        }
    }

    @Test
    fun coldFlow(){
        runBlocking {
            println("Calling simple function...")
            val flow = simple()
            println("Calling collect...")
            flow.collect { value -> println(value) }
            println("Calling collect again...")
            flow.collect { value -> println(value) }
        }
    }

运行结果如下:

Calling simple function...
Calling collect...
Flow started
1
2
3
Calling collect again...
Flow started
1
2
3

从结果可以看到,Flow只有开始收集的时候才开始运行

三、Flow的取消

Flow是可以取消的,这里用withTimeoutOrNull来进行超时取消演示,如下

fun simple(): Flow<Int> = flow { 
    for (i in 1..3) {
        delay(100)          
        println("Emitting $i")
        emit(i)
    }
}

@Test
fun withTimeoutCancel() = runBlocking<Unit> {
    withTimeoutOrNull(250) { // 在 250 毫秒后超时
        simple().collect { value -> println(value) } 
    }
    println("Done")
}

运行结果如下

Emitting 1
1
Emitting 2
2
Done

可以看到超出时间后就取消了。

Flow对每次发射对值进行了ensureActive 监测,意味着可以随时取消。如下

fun foo(): Flow<Int> = flow { 
    for (i in 1..5) {
        println("Emitting $i") 
        emit(i) 
    }
}

fun main() = runBlocking<Unit> {
    foo().collect { value -> 
        if (value == 3) cancel()  
        println(value)
    } 
}

可以看到结果如下

Emitting 1
1
Emitting 2
2
Emitting 3
3
Emitting 4
Exception in thread "main" kotlinx.coroutines.JobCancellationException: BlockingCoroutine was cancelled; job="coroutine#1":BlockingCoroutine{Cancelled}@6d7b4f4c

实际上,由于性能问题,很多其它流操作不会对其进行取消监测。如下

fun main() = runBlocking<Unit> {
    (1..5).asFlow().collect { value -> 
        if (value == 3) cancel()  
        println(value)
    } 
}

结果如下

1
2
3
4
5
Exception in thread "main" kotlinx.coroutines.JobCancellationException: BlockingCoroutine was cancelled; job="coroutine#1":BlockingCoroutine{Cancelled}@3327bd23

可以看到在执行完毕后才会出现异常,并没有按照预期那样取消。因此如果想达到预期效果必须明确表示是否监测取消。可以使用.onEach { currentCoroutineContext().ensureActive() }。但是Flow也提供了现成的操作符 cancellable

fun main() = runBlocking<Unit> {
    (1..5).asFlow().cancellable().collect { value -> 
        if (value == 3) cancel()  
        println(value)
    } 
}

运行结果如下

1
2
3
Exception in thread "main" kotlinx.coroutines.JobCancellationException: BlockingCoroutine was cancelled; job="coroutine#1":BlockingCoroutine{Cancelled}@5ec0a365

四、构建Flow

之前演示了使用flow{}进行构建。除了它还有其它的构建方式

  • flowOf 构建器定义了一个发射固定值集的流。

    flowOf(1, 2, 3)
    
  • 使用 .asFlow() 扩展函数,可以将各种集合与序列转换为流。

    // 将一个整数区间转化为流
    (1..3).asFlow().collect { value -> println(value) }
    

五、过度流操作符

FlowLiveData除了线程切换的区别外,还有一个最大的特点就是,Flow拥有众多操作符可以对数据进行操作。过渡操作符应用于上游流,并返回下游流。而且这些操作符不是挂起函数,返回很快,返回新的Flow内容。

基础操作符和集合一样拥有类似的名字,如 mapfilter。而且这些操作符中可以执行挂起操作,这是序列Sequence所不能做到的

如下所示:

suspend fun performRequest(request: Int): String {
    delay(1000) // 模仿长时间运行的异步工作
    return "response $request"
}

fun main() = runBlocking<Unit> {
    (1..3).asFlow() // 一个请求流
        .map { request -> performRequest(request) }
        .collect { response -> println(response) }
}

结果如下

response 1
response 2
response 3

六、转换操作符

转换操作符可以对数据进行比map更复杂的操作。最常用的为transform。比如

使用 transform 我们可以在执行长时间运行的异步请求之前发射一个字符串并跟踪这个响应:

suspend fun performRequest(request: Int): String {
    delay(1000) // 模仿长时间运行的异步任务
    return "response $request"
}

fun main() = runBlocking<Unit> {
    (1..3).asFlow() // 一个请求流
        .transform { request ->
            emit("Making request $request") 
            emit(performRequest(request)) 
        }
        .collect { response -> println(response) }
}

结果如下

Making request 1
response 1
Making request 2
response 2
Making request 3
response 3

从结果可以看到每次都会发送两条数据,当然也可以根据其它情况然后按需发送,比如只发送偶数的数据

七、限长操作符

限长操作符顾名思义就是限制发射长度,假如有五个数据,可以限制发送两个,但是该操作符会出现异常,所以需要进行处理

fun numbers(): Flow<Int> = flow {
    try {                          
        emit(1)
        emit(2) 
        println("This line will not execute")
        emit(3)    
    } finally {
        println("Finally in numbers")
    }
}

fun main() = runBlocking<Unit> {
    numbers() 
        .take(2) // 只获取前两个
        .collect { value -> println(value) }
}            

结果如下

1
2
Finally in numbers

八、末端流操作

在流结束的时候我们也可以做各种操作,collect()只是其中的基本操作,还有一些其它操作,如

  • 转化为各种集合,例如 toList 与 toSet。
  • 获取第一个(first)值与确保流发射单个(single)值的操作符。
  • 使用 reduce 与 fold 将流规约到单个值。
val sum = (1..5).asFlow()
    .map { it * it } // 数字 1 至 5 的平方                        
    .reduce { a, b -> a + b } // 求和(末端操作符)
println(sum)

结果如下

55

九、流是连续的

流的每次单独收集都是按顺序执行的,除非进行特殊操作的操作符使用多个流。该收集过程直接在协程中运行,该协程调用末端操作符。 默认情况下不启动新协程。 从上游到下游每个过渡操作符都会处理每个发射出的值然后再交给末端操作符。所以假设有五个值,那么会进行五次收集,每次收集就会执行所有流程。这里我们通过一个例子进行演示,该示例过滤偶数并将其映射到字符串:

(1..5).asFlow()
    .filter {
        println("Filter $it")
        it % 2 == 0              
    }              
    .map { 
        println("Map $it")
        "string $it"
    }.collect { 
        println("Collect $it")
    }    

其结果如下

Filter 1
Filter 2
Map 2
Collect string 2
Filter 3
Filter 4
Map 4
Collect string 4
Filter 5

可以看出,假如条件不符合就不会执行map操作

十、Flow上下文

Flow的收集过程总是在协程的上下文中执行。例如,如果有一个流 simple,然后以下代码在它的编写者指定的上下文中运行,而无论流 simple 的实现细节如何,通常情况下,流的上下文不允许切换。流的该属性称为 上下文保存,如下

fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")

withContext(context) {
    simple().collect { value ->
        println(value) // 运行在指定上下文中
    }
}

@Test
fun main() = runBlocking<Unit> {
    simple().collect { value -> log("Collected $value") } 
}    

结果如下

[main @coroutine#1] Started simple flow
[main @coroutine#1] Collected 1
[main @coroutine#1] Collected 2
[main @coroutine#1] Collected 3

由于 simple().collect 是在主线程调用的,那么 simple 的流主体也是在主线程调用的。 这是快速运行或异步代码的理想默认形式,它不关心执行的上下文并且不会阻塞调用者。

十一、withContext 发出错误

然而有时候有些需要在后台线程运行,更新ui需要在主线程运行,在这里贸然使用withContext就会报错. flow {...} 构建器中的代码必须遵循上下文保存属性,并且不允许从其他上下文中发射(emit)。所以下述代码就会出错

fun simple(): Flow<Int> = flow {
    // 在流构建器中更改消耗 CPU 代码的上下文的错误方式
    kotlinx.coroutines.withContext(Dispatchers.Default) {
        for (i in 1..3) {
            Thread.sleep(100) // 假装我们以消耗 CPU 的方式进行计算
            emit(i) // 发射下一个值
        }
    }
}

fun main() = runBlocking<Unit> {
    simple().collect { value -> println(value) } 
}     

大致错误如下

Flow invariant is violated:
		Flow was collected in [CoroutineId(1), "coroutine#1":BlockingCoroutine{Active}@5cbe206b, BlockingEventLoop@5ae98b8c],
		but emission happened in [CoroutineId(1), "coroutine#1":DispatchedCoroutine{Active}@78f05337, Dispatchers.Default].
		Please refer to 'flow' documentation or use 'flowOn' instead
java.lang.IllegalStateException: Flow invariant is violated:

十二、flowOn 操作符

根据上述异常可以知道通过flowOn进行上下文切换。

fun simple(): Flow<Int> = flow {
    for (i in 1..3) {
        Thread.sleep(100) // 假装我们以消耗 CPU 的方式进行计算
        log("Emitting $i")
        emit(i) // 发射下一个值
    }
}.flowOn(Dispatchers.Default) // 在流构建器中改变消耗 CPU 代码上下文的正确方式

fun main() = runBlocking<Unit> {
    simple().collect { value ->
        log("Collected $value") 
    } 
}   

这里需要注意的是flowOn创建了新的协程

十三、缓冲

如果Flow在生产和收集阶段都很耗时间,可以使用缓冲技术,如

fun simple(): Flow<Int> = flow {
    for (i in 1..3) {
        delay(100) // 假装我们异步等待了 100 毫秒
        emit(i) // 发射下一个值
    }
}

fun main() = runBlocking<Unit> { 
    val time = measureTimeMillis {
        simple().collect { value -> 
            delay(300) // 假装我们花费 300 毫秒来处理它
            println(value) 
        } 
    }   
    println("Collected in $time ms")
}

这个例子会很慢,因为生产时候耗时100ms,收集时候耗时300ms,所以我们可以通过缓冲技术,修改如下

val time = measureTimeMillis {
    simple()
        .buffer() // 缓冲发射项,无需等待
        .collect { value -> 
            delay(300) // 假装我们花费 300 毫秒来处理它
            println(value) 
        } 
}   
println("Collected in $time ms")

它产生了相同的数字,只是更快了,由于我们高效地创建了处理流水线, 仅仅需要等待第一个数字产生的 100 毫秒以及处理每个数字各需花费的 300 毫秒。这种方式大约花费了 1000 毫秒来运行,主要就是将生产和收集并行运行

十四、合并

当流代表部分操作结果或操作状态更新时,可能没有必要处理每个值,而是只处理最新的那个。在本示例中,当收集器处理它们太慢的时候, conflate 操作符可以用于跳过中间值。构建前面的示例:

val time = measureTimeMillis {
    simple()
        .conflate() // 合并发射项,不对每个值进行处理
        .collect { value -> 
            delay(300) // 假装我们花费 300 毫秒来处理它
            println(value) 
        } 
}   
println("Collected in $time ms")

结果如下

1
3
Collected in 758 ms

虽然第一个数字仍在处理中,但第二个和第三个数字已经产生,因此第二个是 conflated ,只有最新的(第三个)被交付给收集器,所以知道假如之前生产的内容没有被消费时候,就会被舍弃。

十五、处理最新值

conflate()是通过舍弃生产的值达到快速处理的目的的。另一种是通过取消收集器,每次发射新值时候再次启动。这里可以使用collectLatest来进行操作

val time = measureTimeMillis {
    simple()
        .collectLatest { value -> // 取消并重新发射最后一个值
            println("Collecting $value") 
            delay(300) // 假装我们花费 300 毫秒来处理它
            println("Done $value") 
        } 
}   
println("Collected in $time ms")

十六、Zip

Flow的处理中可以将多个Flow合并在一起,称之为组合。比如Zip就是一种简单的组合方式,效果如下

val nums = (1..3).asFlow() // 数字 1..3
val strs = flowOf("one", "two", "three") // 字符串
nums.zip(strs) { a, b -> "$a -> $b" } // 组合单个字符串
    .collect { println(it) } // 收集并打印

结果如下

1 -> one
2 -> two
3 -> three

十七、Combine

我们知道conflation操作符,假如一个Flow根据另外一个Flow的最新值进行变动时候,这时候使用zip可能会出现问题。如下

    @Test
    fun moreFlowConflate(){
        runBlocking {
            val nums = (1..3).asFlow().onEach { delay(300) } // 发射数字 1..3,间隔 300 毫秒
            val strs = flowOf("one", "two", "three").onEach { delay(400) } // 每 400 毫秒发射一次字符串
            val startTime = System.currentTimeMillis() // 记录开始的时间
            nums.zip(strs) { a, b -> "$a -> $b" } // 使用“zip”组合单个字符串
                .collect { value -> // 收集并打印
                    println("$value at ${System.currentTimeMillis() - startTime} ms from start")
                }
        }
    }

该代码一个300ms发射一个数字,一个是400ms更新一次字符串。合并后的预期效果应该是,每一个更新都会触发另外一个更新。比如300ms后发射数字后会打印一次值,100ms后轮到字符串发射了,又会更新一次字符串,然后200ms后又轮到数字发射了,这时候字符串还没有发射,所以以应该会产生两个相同的字符串。但是实际上并不是。实际结果如下

1 -> one at 430 ms from start
2 -> two at 830 ms from start
3 -> three at 1232 ms from start

解决方式如下

    @Test
    fun moreFlowConflate(){
        runBlocking {
            val nums = (1..3).asFlow().onEach { delay(300) } // 发射数字 1..3,间隔 300 毫秒
            val strs = flowOf("one", "two", "three").onEach { delay(400) } // 每 400 毫秒发射一次字符串
            val startTime = System.currentTimeMillis() // 记录开始的时间
            nums.combine(strs) { a, b -> "$a -> $b" } // 使用“combine”组合单个字符串
                .collect { value -> // 收集并打印
                    println("$value at ${System.currentTimeMillis() - startTime} ms from start")
                }
        }
    }

结果如下

1 -> one at 452 ms from start
2 -> one at 651 ms from start
2 -> two at 854 ms from start
3 -> two at 952 ms from start
3 -> three at 1256 ms from start

十八、flatMapConcat 与 flattenConcat

像集合一样可能存在集合嵌套,需要我们进行平铺。Flow因为操作符可以进行异步请求,所以也会出现类似的情况。如下

fun requestFlow(i: Int): Flow<String> = flow {
    emit("$i: First") 
    delay(500) // 等待 500 毫秒
    emit("$i: Second")    
}
(1..3).asFlow().map { requestFlow(it) }

但是Flow并不能像以前的集合一样使用 flatten 与 flatMap 。需要使用 flatMapConcat 与 flattenConcat 来进行处理,如下

val startTime = System.currentTimeMillis() // 记录开始时间
(1..3).asFlow().onEach { delay(100) } // 每 100 毫秒发射一个数字 
    .flatMapConcat { requestFlow(it) }                                                                           
    .collect { value -> // 收集并打印
        println("$value at ${System.currentTimeMillis() - startTime} ms from start") 
    } 

结果如下

1: First at 121 ms from start
1: Second at 622 ms from start
2: First at 727 ms from start
2: Second at 1227 ms from start
3: First at 1328 ms from start
3: Second at 1829 ms from start

十九、flatMapMerge 与 flattenMerge

上述的效率是按顺序执行的,速度可能会比较慢。FLow也支持并行收集所有传入的流。这里可以使用flatMapMerge 与 flattenMerge 来实现。并发的话需要对并发的流的个数concurrency进行限制,默认为 DEFAULT_CONCURRENCY。

val startTime = System.currentTimeMillis() // 记录开始时间
(1..3).asFlow().onEach { delay(100) } // 每 100 毫秒发射一个数字 
    .flatMapMerge { requestFlow(it) }                                                                           
    .collect { value -> // 收集并打印
        println("$value at ${System.currentTimeMillis() - startTime} ms from start") 
    } 

结果如下

1: First at 136 ms from start
2: First at 231 ms from start
3: First at 333 ms from start
1: Second at 639 ms from start
2: Second at 732 ms from start
3: Second at 833 ms from start

注意:

flatMapMerge 会顺序调用代码块(本示例中的 { requestFlow(it) }),但是并发收集结果流,相当于执行顺序是首先执行 map { requestFlow(it) } 然后在其返回结果上调用 flattenMerge。

二十、flatMapLatest

collectLatest一样,平铺操作中也有类似的操作flatMapLatest

val startTime = System.currentTimeMillis() // 记录开始时间
(1..3).asFlow().onEach { delay(100) } // 每 100 毫秒发射一个数字 
    .flatMapLatest { requestFlow(it) }                                                                           
    .collect { value -> // 收集并打印
        println("$value at ${System.currentTimeMillis() - startTime} ms from start") 
    } 

结果如下

1: First at 142 ms from start
2: First at 322 ms from start
3: First at 425 ms from start
3: Second at 931 ms from start

二十一、Flow的异常

如果Flow的运行出现错误时,可以对其进行处理,这里使用try{}catch{}进行处理

fun simple(): Flow<Int> = flow {
    for (i in 1..3) {
        println("Emitting $i")
        emit(i) // 发射下一个值
    }
}

fun main() = runBlocking<Unit> {
    try {
        simple().collect { value ->         
            println(value)
            check(value <= 1) { "Collected $value" }
        }
    } catch (e: Throwable) {
        println("Caught $e")
    } 
}     

二十二、异常的透明性

如果在发射emit时候出现异常该怎么办呢?使用try{}catch{}。但是被处理掉的代码外部无法得知,除非使用emit把异常Exeption发射出去。所以大致有以下几种方式

  • 可以使用 throw 重新抛出异常。
  • 可以使用 catch 代码块中的 emit 将异常转换为值发射出去。
  • 可以将异常忽略,或用日志打印,或使用一些其他代码处理它。
simple()
    .catch { e -> emit("Caught $e") } // 发射一个异常
    .collect { value -> println(value) }

我么可以捕获发射时候的异常,但是假如收集collect() 出现异常是否依然可以这样。看以下代码

fun simple(): Flow<Int> = flow {
    for (i in 1..3) {
        println("Emitting $i")
        emit(i)
    }
}

fun main() = runBlocking<Unit> {
    simple()
        .catch { e -> println("Caught $e") } // 不会捕获下游异常
        .collect { value ->
            check(value <= 1) { "Collected $value" }                 
            println(value) 
        }
}  

期待打印出catch{}中的“Caught …”异常消息,实际上并非如此

Emitting 1
1
Emitting 2
Exception in thread "main" java.lang.IllegalStateException: Collected 2
    at ...

这里需要使用onEach进行帮助,并保证collect没有参数

simple()
    .onEach { value ->
        check(value <= 1) { "Collected $value" }                 
        println(value) 
    }
    .catch { e -> println("Caught $e") }
    .collect()

注意该代码顺序,这样就看到以下结果

Emitting 1
1
Emitting 2
Caught java.lang.IllegalStateException: Collected 2

二十三、完成情况

当一个Flow完成时候,可以使用这两个方式处理。

  • 命令式 finally 块

    fun simple(): Flow<Int> = (1..3).asFlow()
    
    fun main() = runBlocking<Unit> {
        try {
            simple().collect { value -> println(value) }
        } finally {
            println("Done")
        }
    }            
    
  • onCompletion 操作符

    simple()
        .onCompletion { println("Done") }
        .collect { value -> println(value) }
    

    该操作符与catch操作符配合使用可以知道Flow是正常结束还是异常结束

    fun simple(): Flow = flow {
        emit(1)
        throw RuntimeException()
    }
    
    fun main() = runBlocking {
        simple()
            .onCompletion { cause -> if (cause != null) println("Flow completed exceptionally") }
            .catch { cause -> println("Caught exception") }
            .collect { value -> println(value) }
    }            
    

    onCompletion接收一个参数cause。当上游流没有错误时候,会传一个null

二十四、collect和launchIn

在实际使用中,我么会发现collect会阻塞下一步的运行,比如以下方式

fun simple(): Flow<Int> = flow { // 流构建器
        for (i in 1..4) {
            delay(100) // 假装我们在这里做了一些有用的事情
            emit(i) // 发送下一个值
        }
}  
@Test
    fun coldFlow(){
        runBlocking {
            println("Calling simple function...")
            val flow = simple()
            println("Calling collect...")
            flow.collect { value -> println(value) }
            println("Calling collect...end")
        }
    }

结果如下

Calling simple function...
Calling collect...
1
2
3
4
Calling collect...end

当然也可以使用将Flow包括在一个新的协程里面,不过Flow已经对此作了处理。使用launchIn即可完成该功能。不过由于launchIn只能传入一个协程作用域,因此里面不能像collect一样执行代码,所以这里需要使用onEach { ... }进行配合,实际上onEach { ... }.launchIn(scope)也是成对出现的

fun simple(): Flow = flow { // 流构建器
        for (i in 1..4) {
            delay(100) // 假装我们在这里做了一些有用的事情
            emit(i) // 发送下一个值
        }
}  
@Test
    fun coldFlow(){
        runBlocking {
            println("Calling simple function...")
            val flow = simple()
            println("Calling collect...")
            flow.onEach { value -> println(value)}.launchIn(this)
            println("Calling collect...end")
        }
    }

二十五、参考链接

  1. kotlin协程的数据流

    https://www.kotlincn.net/docs/reference/coroutines/flow.html

  2. transform

    https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/transform.html

你可能感兴趣的:(kotlin)