Kotlin协程和在Android中的使用总结(五 Collection、Sequence、Flow与RxJava对比(上))

在这里插入图片描述

0 Collection和Sequence

在Kotlin中表示多个值时,我们会使用集合Collection和序列Sequence来表示,现在说一下这两者的区别和使用注意事项。

首选这两者的调用都会阻塞当前的线程。如果是在主线程调用,那就会阻塞主线程,可能会引起卡顿。

关于Sequence操作符的一些基础知识点,中间操作符intermediate operator末端操作符terminal operator

intermediate operatormap、distinct、filter、groupBy
terminal operator:* first、toList、count * 等

这些操作汇总:
https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/#functions

Collection的特性

  • 立即执行
  • 每一步调用的转换都会生成一个新的集合(数据量大时会耗内存)
  • 每一步调用的转换都是inline函数,所以不会为调用参数lambda生成Function对象,可以减少内存占用
  • 每一步的转换都是全部完成之后,才会进行下一个转换的计算

Sequence的特性

  • 延迟执行 只有在末端操作符调用时,才开始顺序执行所有变换操作
  • 每一步调用的转换都会生成一个新的Sequence(但是只在调用终端操作符后才会开始计算)
  • 每一步调用的中间操作符转换都是inline函数,存储转换函数到一个列表中,用于最后在终端函数调用时执行。
  • 会对Sequence中的一个元素分别执行完所有变换,然后在对下一个元素进行同样流程的转换计算
    在这里插入图片描述

⚠️注意点:
使用两者时,要注意操作符的调用先后顺序带来的影响。如果前后两个操作的结果没有依赖关系,可以考虑调整顺序,以节省计算资源。


1 suspend function

跟前面的Collection和Sequence不同,suspend function不会阻塞调用线程,这点前面几篇文章已经对其做过详细说明了,这里只举个返回一个List对象的例子:

suspend fun foo(): List<Int> {
    delay(1000) // pretend we are doing something asynchronous here
    return listOf(1, 2, 3)
}

fun main() = runBlocking<Unit> {
    foo().forEach { value -> println(value) } 
}

2 Flow

在上面suspend function的返回值List对象,只能一次性返回所有的值,那么如何能表示一个Stream流的多个异步计算结果呢,这就是本文的重点Flow,就像Sequence<Int>会顺序返回计算结果一样,flow用来表示多个异步返回的Sequence值,可以使用Flow<Int>来表示,同时在flow{ }中可以调用挂起函数

              
fun foo(): Flow<Int> = flow { // flow构建器  foo函数不再使用suspend修饰
    for (i in 1..3) {
        delay(100) // 假装这里发生了耗时的逻辑计算操作,通过调用挂起函数来达到非阻塞线程
        emit(i) // 使用emit来发出多个值,可以多次调用emit
    }
}

fun main() = runBlocking<Unit> {
    // 通过在主线程中调用另一个并发的协程,来说明当前主线程没有被阻塞
    launch {
        for (k in 1..3) {
            println("I'm not blocked $k")
            delay(100)
        }
    }
    // 使用Collect 来收集 flow 产生的数据
    foo().collect { value -> println(value) } 
}

输出log如下:

I’m not blocked 1
1
I’m not blocked 2
2
I’m not blocked 3
3


flow的延迟计算

只有在调用collect时,flow{ }中的代码才会执行,且可以多次对其调用collect,如下:

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

fun main() = runBlocking<Unit> {
    println("Calling foo...")
    val flow = foo()
    println("Calling collect...")
    flow.collect { value -> println(value) } 
    println("Calling collect again...")
    flow.collect { value -> println(value) } 
}

输出log如下:

Calling foo…
Calling collect…
Flow started
1
2
3
Calling collect again…
Flow started
1
2
3

由于flow的延迟计算特性,在执行到*val flow = foo()*时就立即返回了,这也就是flow不用使用suspend修饰的原因。


flow的取消

flow中调用挂起函数后,则该flow在进行collection收集时可以被取消,因为内部调用的挂起函数是可以取消的,如下:

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

fun main() = runBlocking<Unit> {
    withTimeoutOrNull(250) { // Timeout after 250ms 
        foo().collect { value -> println(value) } 
    }
    println("Done")
}

输出log如下:

Emitting 1
1
Emitting 2
2
Done


flow builders构建器

除了上面的flow{ }构建器之外,还有以下两种:

  • fun flowOf(vararg elements: T): Flow (source)
    根据参数elements生成固定的flow,如:
flowOf(1, 2, 3)
  • 多种集合Collection和Sequence对象的扩展函数asFlow()
    如:
(1..3).asFlow().collect { value -> println(value) }

中间操作符 intermediate operator

和collections 、 sequences可以使用中间操作符一样,flow也可以调用中间操作符,每次调用都会立即返回,并生成一个新的转换过的flow,但是和sequence中调用的中间操作符不同的是,flow调用的intermediate operator可以在内部使用挂起函数。如:

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

fun main() = runBlocking<Unit> {
    (1..3).asFlow() // a flow of requests
        .map { request -> performRequest(request) }
        .collect { response -> println(response) }
}

每隔一秒后会打印一行log。


Transform operator

在众多的中间转换操作符当中,transform是最常用的一个,可以用来模拟map、filter,甚至自定义的更复杂的转换操作,可以使用emit发射任意值,如:

(1..3).asFlow() // a flow of requests
    .transform { request ->
        emit("Making request $request") 
        emit(performRequest(request)) 
    }
    .collect { response -> println(response) }

log如下:

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

Size-limiting operators

限制大小的中间操作符,如take会在满足条件后取消flow的执行,由于在协程体中取消会抛出异常,所以可以在flow{ }中使用try-finally来捕获异常执行后续的逻辑,如下:

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) // take only the first two
        .collect { value -> println(value) }
}  

输出log如下:

1
2
Finally in numbers


末端操作符Terminal flow operators

flow中的末端操作符都是挂起函数suspend function,除了collect之外,还有如下:

  • toList toSet 将flow转换成各种集合
  • single、first
  • reduce、flod

如以下代码直接输出一个数字: 55

val sum = (1..5).asFlow()
    .map { it * it } // squares of numbers from 1 to 5                           
    .reduce { a, b -> a + b } // sum them (terminal operator)
println(sum)

Flow context

flow的执行在调用它的线程上,如:

fun foo(): Flow<Int> = flow {
    log("Started foo flow")
    for (i in 1..3) {
        emit(i)
    }
}  

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

打印的log:

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

这在能够快速执行完或者是异步的代码调用,且不阻塞当前线程时没有问题,但是通常我们都会在flow中执行耗时操作,这时候我们可以使用flowOn函数来指定flow中代码的执行线程环境,调用后会为flow的upstream创建一个新的协程。

fun foo(): Flow<Int> = flow {
    for (i in 1..3) {
        Thread.sleep(100) // 假设在执行CPU耗时任务
        log("Emitting $i")
        emit(i) // emit next value
    }
}.flowOn(Dispatchers.Default) // 切换了flow的执行环境,从主线程切到了Default线程池

fun main() = runBlocking<Unit> {
    foo().collect { value ->
        log("Collected $value")  //虽然flow在后台线程,但是collect并发的执行在主线程
    } 
} 

缓冲Buffering

类似于RxJava的背压Flowable,flow中当emit发射过快,collect收集过慢时,可以使用buffer,该操作符会对flow高效地创建一个处理的管道,用以缓存flow中发射的元素。

使用前总耗时为1200ms,

fun foo(): Flow<Int> = flow {
    for (i in 1..3) {
        delay(100) // pretend we are asynchronously waiting 100 ms
        emit(i) // emit next value
    }
}

fun main() = runBlocking<Unit> { 
    val time = measureTimeMillis {
        foo().collect { value -> 
            delay(300) // pretend we are processing it for 300 ms
            println(value) 
        } 
    }   
    println("Collected in $time ms")
}

使用buffer操作符后,总耗时为1000ms, 100+300*3

val time = measureTimeMillis {
    foo()
        .buffer() // buffer emissions, don't wait
        .collect { value -> 
            delay(300) // pretend we are processing it for 300 ms
            println(value) 
        } 
}   
println("Collected in $time ms")

这里举官方文档中的例子:

flowOf("A", "B", "C")
    .onEach  { println("1$it") }
    .collect { println("2$it") }

调用该flow的协程Q中,输出为:

Q : -->-- [1A] – [2A] – [1B] – [2B] – [1C] – [2C] -->–

buffer的流程如下:

flowOf("A", "B", "C")
    .onEach  { println("1$it") }
    .buffer()  // <--------------- buffer between onEach and collect
    .collect { println("2$it") }
P : -->-- [1A] -- [1B] -- [1C] ---------->--  // flowOf(...).onEach { ... }

                      |
                      | channel               // buffer()
                      V

Q : -->---------- [2A] -- [2B] -- [2C] -->--  // collect

即在调用buffer操作符之前的操作会在一个协程P中处理,同时在调用方协程Q中执行collect,两个协程并发执行,通过buffer创建的channel传输元素,当协程P产生元素过快,导致channel中元素满了时,协程P就会被suspend挂起,直到协程Q能处理完元素。

⚠️注意:
上面的代码中,flow的执行体没有切换CoroutineDispatcher,所以默认是在调用者协程context中执行,如果使用能够切换线程的flowOn操作符,该buffer缓冲机制是一样适用的。

合并Conflation

当一个flow只表示一个操作的部分结果,或者操作的状态更新了,可能就不需要处理flow中的每一个值,可以只用处理最新的一个值,所以当collector处理速度太慢时,可以使用conflate 操作符来跳过flow中间产生的值,

val time = measureTimeMillis {
    foo()
        .conflate() // conflate emissions, don't process each one
        .collect { value -> 
            delay(300) // pretend we are processing it for 300 ms
            println(value) 
        } 
}   
println("Collected in $time ms")

当处理第一个值时,第二和第三个值已经发射出来了,所以合并掉第二个,只把最新的值即3交给collector处理,输出log:

1
3
Collected in 758 ms

只处理最新的值

上面的conflate操作符其实是丢弃了中间发射的值来使得供需平衡,还有一种方式就是每当flow发射一个新值时,如果当前collector处理太慢,则取消当前的执行,只处理flow发射的最新的值,使用collectLatest来实现该效果:

val time = measureTimeMillis {
    foo()
        .collectLatest { value -> // 每次新来值时,都取消并重启collect流程
            println("Collecting $value") 
            delay(300) // pretend we are processing it for 300 ms
            println("Done $value") 
        } 
}   
println("Collected in $time ms")

输出log:

Collecting 1
Collecting 2
Collecting 3
Done 3
Collected in 741 ms

感觉这个功能可以用于一些点击事件防抖动的处理,在用户快速多次点击时,只取最后一次点击进行逻辑处理。其他的XXXLatest操作符见文末参考链接

合并多个flow

zip 和 combine

Sequence中有zip操作符,RxJava中也有zip操作符用以合并多个Observable。flow也有:

val nums = (1..3).asFlow().onEach { delay(300) } // numbers 1..3 every 300 ms
val strs = flowOf("one", "two", "three").onEach { delay(400) } // strings every 400 ms
val startTime = System.currentTimeMillis() // remember the start time 
nums.zip(strs) { a, b -> "$a -> $b" } // compose a single string with "zip"
    .collect { value -> // collect and print 
        println("$value at ${System.currentTimeMillis() - startTime} ms from start") 
    } 

使用zip合并两个flow时,会分别对两个流合并处理,也就是快的流要等慢的流发射完才能合并,输出log如下:

1 -> one at 471 ms from start
2 -> two at 870 ms from start
3 -> three at 1271 ms from start

val nums = (1..3).asFlow().onEach { delay(300) } // numbers 1..3 every 300 ms
val strs = flowOf("one", "two", "three").onEach { delay(400) } // strings every 400 ms          
val startTime = System.currentTimeMillis() // remember the start time 
nums.combine(strs) { a, b -> "$a -> $b" } // compose a single string with "combine"
    .collect { value -> // collect and print 
        println("$value at ${System.currentTimeMillis() - startTime} ms from start") 
    } 

当使用combine时,每一个合并的flow发射元素时,都会执行一次collect操作,log如下:

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


参考:
https://kotlinlang.org/docs/reference/coroutines/flow.html#suspending-functions

完整的中间操作符和末端操作符见官方文档:
Flow — asynchronous cold stream of elements

发布了82 篇原创文章 · 获赞 86 · 访问量 11万+

猜你喜欢

转载自blog.csdn.net/unicorn97/article/details/105196025