最全Kotlin:Flow 全面详细指南,附带源码解析。,含BATJM大厂

结语

小编也是很有感触,如果一直都是在中小公司,没有接触过大型的互联网架构设计的话,只靠自己看书去提升可能一辈子都很难达到高级架构师的技术和认知高度。向厉害的人去学习是最有效减少时间摸索、精力浪费的方式。

我们选择的这个行业就一直要持续的学习,又很吃青春饭。

虽然大家可能经常见到说程序员年薪几十万,但这样的人毕竟不是大部份,要么是有名校光环,要么是在阿里华为这样的大企业。年龄一大,更有可能被裁。

送给每一位想学习Java小伙伴,用来提升自己。

在这里插入图片描述

本文到这里就结束了,喜欢的朋友可以帮忙点赞和评论一下,感谢支持!

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

job.cancel(CancellationException(“timeout 250”))

println(“done”)

}

结果:看我们只需要取消对应的协程即可,对应的flow也会被取消收集。

emit 1

1

emit 2

2

done

这里引申一点,对于timeout,官方有提供专用的操作函数,withTimeout系列。不需要我们手动delay然后继续调用取消,毕竟不是很优雅。

上述代码也可以写成如下的形式

fun simple(): Flow = flow {

for (i in 1…3) {

delay(100)

println(“emit $i”)

emit(i)

}

}

fun main() = runBlocking {

withTimeoutOrNull(250) {

simple().collect { value -> println(value) }

}

println(“done”)

}

看到了吗?直接将launch替换为withTimeoutOrNull就可以做到延时取消效果了,这里简单做一下源码分析。

源码

public suspend fun withTimeoutOrNull(timeMillis: Long, block: suspend CoroutineScope.() -> T): T? {

try {

return suspendCoroutineUninterceptedOrReturn { uCont ->

val timeoutCoroutine = TimeoutCoroutine(timeMillis, uCont)

coroutine = timeoutCoroutine

setupTimeout<T?, T?>(timeoutCoroutine, block)

}

} catch (e: TimeoutCancellationException) {

}

}

private class TimeoutCoroutine<U, in T: U>(

@JvmField val time: Long,

uCont: Continuation // unintercepted continuation

) : ScopeCoroutine(uCont.context, uCont), Runnable {

override fun run() {

cancelCoroutine(TimeoutCancellationException(time, this))

}

}

private fun <U, T: U> setupTimeout(

coroutine: TimeoutCoroutine<U, T>,

block: suspend CoroutineScope.() -> T

): Any? {

// schedule cancellation of this coroutine on time

coroutine.disposeOnCompletion(context.delay.invokeOnTimeout(coroutine.time, coroutine, coroutine.context))

}

public fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle =

DefaultDelay.invokeOnTimeout(timeMillis, block, context)

省略了一大堆非核心的代码,我们直接看延时取消的操作,这里简单分析一下:

  • 创建TimeoutCoroutine对象,它同时实现了Runnable,在run里面调用了取消函数,抛出TimeoutCancellationException。

  • 调用当前context的invokeOnTimeout函数,该函数需要一个Runnable,传入了timeoutCoroutine。此实现使用内置的单线程调度执行器服务。会在延时对应的事件后调用Runnable的run函数,然后就会取消当前的协程。

  • 在取消协程之后,会取消掉当前上下文的所有将在完成时调用的回调,disposeOnCompletion函数被调用。

Flow 相关操作符

这一块的操作符,其实是比较多的。但是如果您熟悉RxJava的话,其实都是差不多的。这一块这里就不做源码分析了,只是看一下怎么使用即可。内部其实是创建了新的流返回出来了,有兴趣的话可以自行查看一下源码。

中间流操作符
map

映射。看🌰(传入请求流可以使用 map 操作符映射到结果,即使执行请求是由挂起函数实现的长时间运行的操作)

suspend fun performRequest(request: Int): String {

delay(1000) // 模拟长时间的异步工作

return “response $request”

}

fun main() = runBlocking {

(1…3).asFlow() // 转换为flow

.map { request -> performRequest(request) }

.collect { response -> println(response) }

}

结果

response 1

response 2

response 3

filter

过滤操作,看🌰

suspend fun performRequest(request: Int): String {

delay(1000) // 模拟长时间的异步工作

return “response $request”

}

fun main() = runBlocking {

(1…3).asFlow() // 转换为flow

.map { request -> performRequest(request) }

.filter { it == “response 1” }

.collect { response -> println(response) }

}

结果:仅返回匹配到的值

response 1

变换操作符

在流变换算子中,最通用的一种叫做变换。 它可以用来模仿简单的转换,比如 map 和 filter,也可以实现更复杂的转换。 使用transform,我们可以发出任意次数的任意值。

例如🌰,使用transform,我们可以在执行长时间运行的异步请求之前发出一个字符串,并在其后响应:

(1…3).asFlow() // a flow of requests

.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

size限制操作符

顾名思义,限制收集的数量。使用运算符take

它会在判断当发射的值在达到相应限制时取消流程的执行。 因为协程中的取消总是通过抛出异常来执行。所以需要考虑进行相应的异常捕获来保证后续的流畅正常进行不被取消掉

fun numbers(): Flow = flow {

try {

emit(1)

emit(2)

println(“not execute”)

emit(3)

} finally {

println(“finally in numbers”)

}

}

fun main() = runBlocking {

numbers()

.take(2) // take 两个值

.collect { value -> println(value) }

}

结果

1

2

finally in numbers

终端操作符

终端操作符可以启动一个流,最基础的就是上述常提到的collect。但是还有一些其他的终端操作符,它可能会让一些操作变得更简单:

转换为各种集合, toList 和 toSet。

flowOf(1,2).toList().forEach {

println(it)

}

flowOf(1,2).toSet().forEach(::println)

first , 确保获取且进获取第一个值

返回流发出的第一个元素然后取消流的集合的终端运算符。 如果流为空,则抛出 NoSuchElementException。

val value : Int = flowOf(1, 2).first()

println(value)

使用 reduce 和 fold 将流合并到一个值。

val sum = (1…5).asFlow()

.map { it * it } //平方

.reduce { a, b -> a + b } // 进行累加

println(sum)

//结果

55

fold和reduce使用起来差不多,区别就是fold可以定义初始化,其实很简单,reduce传入的lambda前一个参数是每次计算的结果累计,后一个参数是当前需要传入的值,不明白可以去瞅一眼源码,这里不在引申。

onEach

这个操作符也较为常用,这里也介绍一下,返回在上游流的每个值向下游发出之前调用给定操作的流。

🌰

(1…5)

.asFlow()

.onEach {

println(“onEach$it”)

}.collect()

//结果

onEach1

onEach2

onEach3

onEach4

onEach5

操作符的顺序

除非使用对多个流进行操作的特殊运算符,否则流的每个单独集合都按顺序执行。 该集合直接在调用终端运算符的协程中工作。 默认情况下不会启动新的协程。 每个发出的值都由从上游到下游的所有中间操作符处理,然后交付给终端操作符。

🌰

(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

Flow 调度器切换

对于UI驱动型的程序来说,需要将长时间计算的任务放在异步线程处理,UI展示工作需要放在主线程处理。也就是说需要将构建器的代码放到异步线程执行,但是终端操作符,比如collect需要在主线程获取,那么怎么做呢?

使用flowOn操作符

在这之前您可能的了解一下,协程的调度器。可以简单参考之前写的一篇文章,有对调度器做简单介绍:https://blog.csdn.net/weixin_44235109/article/details/119981210

fun main() = runBlocking {

flow {

for (i in 1…3) {

//模拟异步处理

delay(100)

log(“Emitting $i”)

emit(i) // emit next value

}

}.flowOn(Dispatchers.Default)//使用flowOn传入Default的调度器

.collect { value ->

log(“Collected $value”)

}

}

结果

16:39:21:954 [DefaultDispatcher-worker-1] Emitting 1

16:39:21:969 [main] Collected 1

16:39:22:071 [DefaultDispatcher-worker-1] Emitting 2

16:39:22:071 [main] Collected 2

16:39:22:178 [DefaultDispatcher-worker-1] Emitting 3

16:39:22:178 [main] Collected 3

可以很明显的看出,构建模块被调度到异步线程处理了。而收集的工作还在主线程进行。

flowOn负责构建的模块调度,那么收集的谁负责呢?

其实和异常处理类似,collect受调用它的协程上下文限制,所以最后的执行线程和当前协程上下文的调度器有关,目前我使用的是idea测试的,默认runBlocking的调度器就是主线程。如果是android上面的话,runBlocking可能就需要传入Dispatchers.Main了。

其实和RxJava还是非常相似的😂。

注意一点,此时其实已经改变流执行的顺序了

官方的解释如下:

Another thing to observe here is that the flowOn operator has changed the default sequential nature of the flow. Now collection happens in one coroutine (“coroutine#1”) and emission happens in another coroutine (“coroutine#2”) that is running in another thread concurrently with the collecting coroutine. The flowOn operator creates another coroutine for an upstream flow when it has to change the CoroutineDispatcher in its context.

这里要注意的另一件事是 flowOn 运算符更改了流的默认顺序性质。 现在收集发生在一个协程(“coroutine#1”)中,发射发生在另一个协程(“coroutine#2”)中,该协程与收集协程同时运行在另一个线程中。 当必须在其上下文中更改 CoroutineDispatcher 时, flowOn 运算符为上游流创建另一个协程。

这一块我简单看了一下源码,这里面不同的调度器会遇到多线程的问题,最里面使用了channel进行了调度处理。具体的核心类是ChannelFlow。后面会对flow进行简单源码分析,但篇幅有限不对这一块过深入分析,感兴趣可以自行查看,或找博主私下探讨。

其实上面的看不出来会改变流执行的顺序,下面改变一下代码,验证一下看看🌰

fun main() = runBlocking {

flow {

for (i in 1…3) {

//模拟异步处理

delay(100)

log(“Emitting $i”)

emit(i) // emit next value

}

}.flowOn(Dispatchers.Default)

.collect { value ->

delay(200)

log(“Collected $value”)

}

}

结果

16:52:33:258 [DefaultDispatcher-worker-1] Emitting 1

16:52:33:386 [DefaultDispatcher-worker-1] Emitting 2

16:52:33:482 [main] Collected 1

16:52:33:493 [DefaultDispatcher-worker-1] Emitting 3

16:52:33:684 [main] Collected 2

16:52:33:887 [main] Collected 3

我们只需要将,collect里面增加一个delay即可,发现其实这时候就是发射归发射,收集归收集了。不似上面我们写的程序,发射一个值只有到终端操作符之后才会发射第二个。这里面肯定就会对值进行缓存。那么就会牵扯到一个问题。老生常谈🙆‍♀️,背压处理

Flow 背压处理

对于背压处理,Kotlin 提供三种解决方案:

| 操作符 | 含义 |

| — | — |

| buffer | 指定固定容量缓存 |

| conflate | 保留最新的值 |

| collectLatest | 新值发送时取消之前的 |

buffer

这里有必要看一下buffer函数的源码定义

public fun Flow.buffer(capacity: Int = BUFFERED, onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND)

可以看出需要两个参数,都有默认值。

  • 第一个好理解容量,默认等于BUFFERED,这个值其实时64,可以按照使用时需要自己指定具体的数字。

  • 第二个是指定,当buffer溢出时的操作,默认的操作是挂起。还要两个操作分别是删除最旧的值不要挂起或者删除当前最新的值不要挂起,可以自行查看源码,这里不再引申。

使用🌰

flow {

for (i in 1…3) {

//模拟异步处理

delay(100)

log(“Emitting $i”)

emit(i) // emit next value

}

}.flowOn(Dispatchers.Default)

.buffer()

.collect { value ->

delay(200)

log(“Collected $value”)

}

//结果

17:17:28:420 [DefaultDispatcher-worker-1] Emitting 1

17:17:28:536 [DefaultDispatcher-worker-1] Emitting 2

17:17:28:646 [main] Collected 1

17:17:28:646 [DefaultDispatcher-worker-1] Emitting 3

17:17:28:846 [main] Collected 2

17:17:29:049 [main] Collected 3

conflate

这个只获取最新值也比较好理解,应用场景,比如说获取下载进度,对于用户来说其实每次只需要获取当前最新的进度就好了,不需要把之前的值再去获取一遍,下面也举一个例子🌰

fun main() = runBlocking {

flow {

for (i in 1…3) {

//模拟异步处理

delay(100)

log(“Emitting $i”)

emit(i) // emit next value

}

}.flowOn(Dispatchers.Default)

.conflate()

.collect { value ->

delay(300)//模拟下游处理比较慢

log(“Collected $value”)

}

}

//结果 第一个值肯定可以拿到 当地一个值处理完成之后 最新的值就是3了 所以丢弃了2

17:21:42:916 [DefaultDispatcher-worker-1] Emitting 1

17:21:43:034 [DefaultDispatcher-worker-1] Emitting 2

17:21:43:140 [DefaultDispatcher-worker-1] Emitting 3

17:21:43:236 [main] Collected 1

17:21:43:546 [main] Collected 3

collectLatest

说明一点:这玩意其实也是一个终端操作符

前两个可能都比较好理解,那新值发送时取消之前的是什么意思呢?为了便于理解,直接上例子,按照结果说明:

🌰

fun main() = runBlocking {

flow {

for (i in 1…3) {

//模拟异步处理

delay(100)

log(“Emitting $i”)

emit(i) // emit next value

}

}.flowOn(Dispatchers.Default)

.collectLatest {

delay(300)

log(“Collected $it”)

}

}

//结果

17:25:33:916 [DefaultDispatcher-worker-1] Emitting 1

17:25:34:038 [DefaultDispatcher-worker-1] Emitting 2

17:25:34:150 [DefaultDispatcher-worker-1] Emitting 3

17:25:34:453 [main] Collected 3

对比上面的例子,这里只是将collect替换成了collectLatest而已。为什么1没有了呢?

显而易见了,这玩意会在最新的到来会直接取消下游上一个消费的处理,因为有delay所以1还没有来得及打印,就因为下一个值发射了,然后就被取消了!!!您可真霸道呢?🙆‍♀️

Flow 异常处理

当操作符内的发射器或代码抛出异常时,流收集可以以异常结束。 有几种方法可以处理这些异常。

try…catch

较为简单,上🌰

fun simple(): Flow = flow {

for (i in 1…3) {

println(“Emitting $i”)

emit(i) // emit next value

}

}

fun main() = runBlocking {

try {

simple().collect { value ->

println(value)

check(value <= 1) { “Collected $value” }

}

} catch (e: Throwable) {

println(“Caught $e”)

}

}

//结果

fun simple(): Flow = flow {

for (i in 1…3) {

println(“Emitting $i”)

emit(i) // emit next value

}

}

fun main() = runBlocking {

try {

simple().collect { value ->

println(value)

if (value>1){

throw IllegalStateException(“exception value is $value”)

}

}

} catch (e: Throwable) {

println(“Caught $e”)

}

}

显而易见,抛出异常之后,收集结束,如果是UI驱动程序比如:Android还是推荐主从作用域,异常不会向上传播。

思考一个问题,刚刚异常是发生在收集端,如果在构建的时候发生异常呢?

fun simple(): Flow =

flow {

for (i in 1…3) {

println(“Emitting $i”)

emit(i) // emit next value

}

}

.map { value ->

if (value>1){

throw IllegalStateException(“exception value is $value”)

}

“string $value”

}

fun main() = runBlocking {

try {

simple().collect { value -> println(value) }

} catch (e: Throwable) {

println(“Caught $e”)

}

}

//结果

Emitting 1

string 1

Emitting 2

Caught java.lang.IllegalStateException: exception value is 2

完美:异常仍被捕获并停止收集

上述代码的问题就是不够优雅,还有异常对于流来说必须是透明的,使用try … catch 显然违反了透明性,所以Kotlin 封装了try catch.

catch 相关运算符

catch 运算符的主体可以分析异常并根据捕获的异常以不同的方式对其做出反应:

  • 可以使用 throw 重新抛出异常。

  • 可以使用 catch 主体中的发射将异常转换为值的发射。

  • 异常可以被其他代码忽略、记录或处理。

看🌰

simple()

.catch { e -> emit(“Caught $e”) } // 我不仅捕获到了异常,我还能继续发射!!!!

.collect { value -> println(value) }

结果

Emitting 1

string 1

Emitting 2

Caught java.lang.IllegalStateException: exception value is 2

当然因为抛出了异常,协程还是会终止,但是此时异常是以发生的形式传递下去的。

注意:catch 中间运算符,尊重异常透明性,只捕获上游异常(即来自 catch 之上的所有运算符的异常,但不在其之下)。 如果 collect { … } 中的块(位于 catch 下方)抛出异常,则它不会捕获:

🌰

fun simple(): Flow = flow {

for (i in 1…3) {

println(“Emitting $i”)

emit(i)

}

}

fun main() = runBlocking {

simple()

.catch { e -> println(“Caught $e”) } // 不会捕获到下游的异常

.collect { value ->

if(value <= 1) throw IllegalStateException(“Collected $value”)

println(value)

}

}

//

Emitting 1

1

Emitting 2

Exception in thread “main” java.lang.IllegalStateException: Collected 2

at org.example.zxf.kotlin11.flow.TestFlowKt$main 1 1 1invokeSuspend$ i n l i n e d inlined inlinedcollect$1.emit(Collect.kt:133)

这个时候可以利用onEach将需要可能捕获异常的地方前置,如下所示。

simple()

.onEach { value ->

check(value <= 1) { “Collected $value” }

println(value)

}

.catch { e -> println(“Caught $e”) }

.collect()

onCompletion 相关操作

我们知道和try … catch 搭配的 还有一个finally。所以 flow当然也有一个差不多的了,叫onCompletion

fun simple(): Flow = flow {

for (i in 1…3) {

println(“Emitting $i”)

emit(i)

}

}

fun main() = runBlocking {

simple()

.onCompletion { println(“Done”) }

.collect { value -> println(value) }

}

//结果

Emitting 1

1

Emitting 2

2

Emitting 3

3

Done

抛个异常试试,onCompletion可以通过cause进行判断是否是正常结束.

fun simple(): Flow = flow {

emit(1)

throw RuntimeException()

emit(2)

}

fun main() = runBlocking {

simple()

.onCompletion { cause ->

if (cause != null) {

println(“Flow completed exceptionally”)

}

}

.catch { cause -> println(“Caught exception”) }

.collect { value -> println(value) }

}

//结果

1

Flow completed exceptionally

Caught exception

与 catch 运算符的另一个区别是 onCompletion 会看到所有异常,并且仅在成功完成上游流(没有取消或失败)时才会收到空异常。

fun simple(): Flow = (1…3).asFlow()

fun main() = runBlocking {

simple()

.onCompletion { cause -> println(“Flow completed with $cause”) }

.collect { value ->

if (value>1){

throw IllegalStateException(“exception value is $value”)

}

println(value)

}

}

//结果

1

Flow completed with java.lang.IllegalStateException: exception value is 2

Exception in thread “main” java.lang.IllegalStateException: exception value is 2

注意一点:虽然看到了,但是并没有进行捕获。异常还是抛出了

独家面经总结,超级精彩

本人面试腾讯,阿里,百度等企业总结下来的面试经历,都是真实的,分享给大家!

image

image

image

image

Java面试准备

准确的说这里又分为两部分:

  1. Java刷题
  2. 算法刷题

Java刷题:此份文档详细记录了千道面试题与详解;

image

image

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

le()

.onCompletion { cause ->

if (cause != null) {

println(“Flow completed exceptionally”)

}

}

.catch { cause -> println(“Caught exception”) }

.collect { value -> println(value) }

}

//结果

1

Flow completed exceptionally

Caught exception

与 catch 运算符的另一个区别是 onCompletion 会看到所有异常,并且仅在成功完成上游流(没有取消或失败)时才会收到空异常。

fun simple(): Flow = (1…3).asFlow()

fun main() = runBlocking {

simple()

.onCompletion { cause -> println(“Flow completed with $cause”) }

.collect { value ->

if (value>1){

throw IllegalStateException(“exception value is $value”)

}

println(value)

}

}

//结果

1

Flow completed with java.lang.IllegalStateException: exception value is 2

Exception in thread “main” java.lang.IllegalStateException: exception value is 2

注意一点:虽然看到了,但是并没有进行捕获。异常还是抛出了

独家面经总结,超级精彩

本人面试腾讯,阿里,百度等企业总结下来的面试经历,都是真实的,分享给大家!

[外链图片转存中…(img-snWzT5Jp-1715575371272)]

[外链图片转存中…(img-9ArQ9fHq-1715575371273)]

[外链图片转存中…(img-s9PR6XlL-1715575371273)]

[外链图片转存中…(img-5zbhmvgZ-1715575371273)]

Java面试准备

准确的说这里又分为两部分:

  1. Java刷题
  2. 算法刷题

Java刷题:此份文档详细记录了千道面试题与详解;

[外链图片转存中…(img-HFmknhL7-1715575371274)]

[外链图片转存中…(img-Oe85Po5O-1715575371274)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

  • 12
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值