一、 Flow 与 Channel 的相互转换

1.1 Flow 转换为 Channel

1.1.1 ChannelFlow

@InternalCoroutinesApi
public abstract class ChannelFlow<T>(
// upstream context
@JvmField public val context: CoroutineContext,
// buffer capacity between upstream and downstream context
@JvmField public val capacity: Int,
// buffer overflow strategy
@JvmField public val onBufferOverflow: BufferOverflow
) : FusibleFlow<T> {
... public open fun produceImpl(scope: CoroutineScope): ReceiveChannel<T> =
scope.produce(context, produceCapacity, onBufferOverflow, start = CoroutineStart.ATOMIC, block = collectToFun) ... }

前面提到 ChannelFlow 是热流。只要上游产生数据,就会立即发射给下游收集者。

ChannelFlow 是一个抽象类,并且被标记为内部 Api,不应该在外部代码直接使用。

注意到它内部有一个方法 produceImpl 返回的是一个 ReceiveChannel,它的实现是收集上游发射的数据,然后发送到 Channel 中。

有此作为基础。我们可以 调用 asChannelFlow 将 Flow 转换 ChannelFlow, 进而转换成 Channel 。

1.1.2 produceIn —— 将 Flow 转换为单播式 Channel

produceIn()转换创建了一个produce 协程来 collect 原Flow,因此该produce协程应该在恰当时候被关闭或者取消。转换后的 Channel 拥有处理背压的能力。其基本使用方式如下:

fun main() = runBlocking {
val flow = flow<Int> {
repeat(5) {
delay(500)
emit(it)
}
} val produceIn = flow.produceIn(this)
for (ele in produceIn) {
println(ele)
}
}

输出结果:

0
1
2
3
4

查看 produceIn 源码:

@FlowPreview
public fun <T> Flow<T>.produceIn(scope: CoroutineScope): ReceiveChannel<T> = asChannelFlow().produceImpl(scope)

1.1.3 broadcastIn —— 将 Flow 转换为广播式 BroadcastChannel。

broadcastIn 转换方式与 produceIn 转换方式实现原理一样,区别是创建出来的 BroadcastChannel。

源码如下:

public fun <T> Flow<T>.broadcastIn(
scope: CoroutineScope,
start: CoroutineStart = CoroutineStart.LAZY
): BroadcastChannel<T> {
// Backwards compatibility with operator fusing
val channelFlow = asChannelFlow()
val capacity = when (channelFlow.onBufferOverflow) {
BufferOverflow.SUSPEND -> channelFlow.produceCapacity
BufferOverflow.DROP_OLDEST -> Channel.CONFLATED
BufferOverflow.DROP_LATEST ->
throw IllegalArgumentException("Broadcast channel does not support BufferOverflow.DROP_LATEST")
}
return scope.broadcast(channelFlow.context, capacity = capacity, start = start) {
collect { value ->
send(value)
}
}
}

使用方式见上文 BroadcastChannel。

和 BroadcastChannel 一样,broadcastIn 也标记为过时的 API, 不建议继续使用了。

1.2 Channel 转换为 Flow

1.2.1 consumeAsFlow/receiveAsFlow —— 将单播式 Channel 转换为 Flow

使用 consumeAsFlow()/receiveAsFlow() 将 Channel 转换为 Flow

fun main() = runBlocking<Unit> {
val testChannel = Channel<String>() val testFlow = testChannel.receiveAsFlow() launch {
testFlow.collect {
println(it)
}
} delay(100)
testChannel.send("hello")
delay(100)
testChannel.send("coroutine")
delay(100) testChannel.close() // 注意只有 Channel 关闭了,协程才能结束
}

查看源码:

public fun <T> ReceiveChannel<T>.consumeAsFlow(): Flow<T> = ChannelAsFlow(this, consume = true)

public fun <T> ReceiveChannel<T>.receiveAsFlow(): Flow<T> = ChannelAsFlow(this, consume = false)

private class ChannelAsFlow<T>(
private val channel: ReceiveChannel<T>,
private val consume: Boolean,
context: CoroutineContext = EmptyCoroutineContext,
capacity: Int = Channel.OPTIONAL_CHANNEL,
onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
) : ChannelFlow<T>(context, capacity, onBufferOverflow) {}

consumeAsFlowreceiveAsFlow 都是调用 ChannelAsFlow 将 Channel 转换成了 ChannelFlow,所以转换结果是热流。但它们传递的第二个参数 consume 不一样。两者区别如下:

  • 使用 consumeAsFlow() 转换成的 Flow 只能有一个收集器收集,如果有多个收集器收集,将会抛出如下异常:
Exception in thread "main" java.lang.IllegalStateException: ReceiveChannel.consumeAsFlow can be collected just once
  • 使用 receiveAsFlow() 转换成的 Flow 可以有多个收集器收集,但是保证每个元素只能被一个收集器收集到,即单播式。

通俗点说,就是使用 consumeAsFlow() 只能有一个消费者。 使用 receiveAsFlow() 可以有多个消费者,但当向 Channel 中发射一个数据之后,收到该元素的消费者是不确定的。

1.2.2 asFlow —— 将广播式 BroadcastChannel 转换为 Flow

与单播式相对的就是广播式,让每个消费者都收到该元素,这就需要一个广播式的 Chanel:BroadcastChanel。

BroadcastChannel 调用 asFlow() 方法即可将其转换为 Flow。

由于该方法也被标记为过时了,替代方案有 SharedFlow 和 StateFlow。

二、SharedIn —— 将冷数据流转换为热数据流

将 flow 转换为 SharedFlow,可以使用 SharedIn 方法:

public fun <T> Flow<T>.shareIn(
scope: CoroutineScope,
started: SharingStarted,
replay: Int = 0
): SharedFlow<T> {
...
}

参数解释:

  • CoroutineScope 用于共享数据流的 CoroutineScope。此作用域函数的生命周期应长于任何使用方,以使共享数据流在足够长的时间内保持活跃状态
  • replay 每个新收集器的数据项数量
  • started “启动” 方式

启动方式有:

public fun interface SharingStarted {
public companion object {
// 立即启动,并且永远不会自动停止
- public val Eagerly: SharingStarted = StartedEagerly() // 第一个订阅者注册后启动,并且永远不会自动停止
- public val Lazily: SharingStarted = StartedLazily() // 第一个订阅者注册后启动,最后一个订阅者取消注册后停止
- public fun WhileSubscribed(
stopTimeoutMillis: Long = 0,
replayExpirationMillis: Long = Long.MAX_VALUE
): SharingStarted =
StartedWhileSubscribed(stopTimeoutMillis, replayExpirationMillis)
}
}

三、callbackFlow —— 将基于回调的 API 转换为数据流

Kotlin 协程和 Flow 可以完美解决异步调用、线程切换的问题。设计接口时,可以类似 Rxjava 那样,避免使用回调。比如 Room 在内的很多库已经支持将协程用于数据流操作。对于那些还不支持的库,也可以将任何基于回调的 API 转换为协程。

callbackFlow 是一个数据流构建器,可以将基于回调的 API 转换为数据流。

3.1 callbackFlow 的使用

举例:

interface Result<T>  {
fun onSuccess(t: T)
fun onFail(msg: String)
} fun getApi(res: Result<String>) {
thread{
printWithThreadInfo("getApiSync")
Thread.sleep(1000) // 模拟耗时任务
res.onSuccess("hello")
}.start()
}

getApi() 是一个基于回调设计的接口。如何使用 callbackFlow 转换为 Flow 呢?

fun getApi(): Flow<String> = callbackFlow {
val res = object: Result<String> {
override fun onSuccess(t: String) {
trySend(t)
close(Exception("completion"))
} override fun onFail(msg: String) {
}
}
getApi(res) // 一定要调用骨气函数 awaitClose, 保证流一直运行。在`awaitClose` 中移除 API 订阅,防止任务泄漏。
awaitClose {
println("close")
}
} // 新的 Api 使用方式
fun main() = runBlocking<Unit> {
getApi().flowOn(Dispatchers.IO)
.catch {
println("getApi fail, cause: ${it.message}")
}.onCompletion {
println("onCompletion")
}.collect {
printWithThreadInfo("getApi success, result: $it")
}
}

这时候你可能有疑问了,这在流的内部不还是使用了基于接口的调用吗,分明没有更方便。看下面的例子,就能体会到了。

3.2 callbackFlow 实战

Android 开发中有一个常见的场景:输入关键字进行查询。比如有个 EditText,输入文字后,基于输入的文字进行网络请求或者数据库查询。

假设查询数据的接口:

fun <T>query(keyWord: String): Flow<T> {
return flow {
//...
}
}

首先定义一个方法将 EditText 内容变化的回调转换成 Flow

fun textChangeFlow(editText: EditText): Flow<String> = callbackFlow {
val watcher = object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { } override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
} override fun afterTextChanged(s: Editable?) {
s?.let {
trySend(s.toString())
} }
}
editText.addTextChangedListener(watcher)
awaitClose {
editText.removeTextChangedListener(watcher)
}
}

使用:

scope.launch{
textChangeFlow(editText)
.debounce(300) // 防抖处理
.flatMapLatest { keyWord -> // 只对最新的值进行搜索
flow {
<String>query(keyWord)
}
}.collect {
// ... 处理最终结果
}
}

在这个过程中,我们可以充分使用 Flow 的各种变换,对我们的中间过程进行处理。实现一些很难实现的需求。

Kotlin 协程四 —— Flow 和 Channel 的应用的更多相关文章

  1. Kotlin协程通信机制: Channel

    Coroutines Channels Java中的多线程通信, 总会涉及到共享状态(shared mutable state)的读写, 有同步, 死锁等问题要处理. 协程中的Channel用于协程间 ...

  2. Kotlin 协程一 —— 全面了解 Kotlin 协程

    一.协程的一些前置知识 1.1 进程和线程 1.1.1基本定义 1.1.2为什么要有线程 1.1.3 进程与线程的区别 1.2 协作式与抢占式 1.2.1 协作式 1.2.2 抢占式 1.3 协程 二 ...

  3. rxjava回调地狱-kotlin协程来帮忙

    本文探讨的是在tomcat服务端接口编程中, 异步servlet场景下( 参考我另外一个文章),用rxjava来改造接口为全流程异步方式 好处不用说 tomcat的worker线程利用率大幅提高,接口 ...

  4. Kotlin协程解析系列(上):协程调度与挂起

    vivo 互联网客户端团队- Ruan Wen 本文是Kotlin协程解析系列文章的开篇,主要介绍Kotlin协程的创建.协程调度与协程挂起相关的内容 一.协程引入 Kotlin 中引入 Corout ...

  5. python并发编程之gevent协程(四)

    协程的含义就不再提,在py2和py3的早期版本中,python协程的主流实现方法是使用gevent模块.由于协程对于操作系统是无感知的,所以其切换需要程序员自己去完成. 系列文章 python并发编程 ...

  6. Kotlin协程第一个示例剖析及Kotlin线程使用技巧

    Kotlin协程第一个示例剖析: 上一次https://www.cnblogs.com/webor2006/p/11712521.html已经对Kotlin中的协程有了理论化的了解了,这次则用代码来直 ...

  7. Retrofit使用Kotlin协程发送请求

    Retrofit2.6开始增加了对Kotlin协程的支持,可以通过suspend函数进行异步调用.本文简单介绍一下Retrofit中协程的使用 导入依赖 app的build文件中加入: impleme ...

  8. Kotlin协程基础

    开发环境 IntelliJ IDEA 2021.2.2 (Community Edition) Kotlin: 212-1.5.10-release-IJ5284.40 我们已经通过第一个例子学会了启 ...

  9. Android Kotlin协程入门

    Android官方推荐使用协程来处理异步问题.以下是协程的特点: 轻量:单个线程上可运行多个协程.协程支持挂起,不会使正在运行协程的线程阻塞.挂起比阻塞节省内存,且支持多个并行操作. 内存泄漏更少:使 ...

  10. Kotlin Coroutine(协程): 四、+ Retrofit

    @ 目录 前言 一.准备工作 二.开始使用 1.简单使用 2.DSL 3.扩展函数 4.请求发起 总结 前言 Retrofit 从 2.6.0 版本开始, 内置了对 Kotlin Coroutines ...

随机推荐

  1. 【转帖】Mysql一张表可以存储多少数据

    https://www.cnblogs.com/wenbochang/p/16723537.html Mysql一张表可以存储多少数据 在操作系统中,我们知道为了跟磁盘交互,内存也是分页的,一页大小4 ...

  2. [转帖]关于Nacos默认token.secret.key及server.identity风险说明及解决方案公告

    https://nacos.io/zh-cn/blog/announcement-token-secret-key.html 近期Nacos社区收到关于Nacos鉴权功能通过token.secret. ...

  3. [转帖]【dperf系列-5】使用dperf进行性能测试(初级)

    https://zhuanlan.zhihu.com/p/451341132 dperf是一款高性能的开源网络压力测试仪,是Linux基金会旗下的DPDK官方生态项目.本文介绍如利用dperf在两台物 ...

  4. [转帖]如何优雅的使用 Systemd 管理服务

    https://zhuanlan.zhihu.com/p/271071439 背景:我们在构建 Kubernetes 容器化平台时,会在节点上部署各种 agent ,虽然容器化当道的今天很多程序可以直 ...

  5. void的讲解 、any的讲解 、联合类型的讲解

    1. void的使用 空值一般采用 void 来表示,同时void也可以表示变量 也可以表示函数没有返回值哈 使用了 void 就不能够使用 return 哈 let sum = function() ...

  6. Linux命令行从x度网盘下载数据

    技术背景 做开源项目的时候,尤其是现在的数据量越来越大,经常会面临到数据往哪里存放的问题.因为自己刚好有一个某度云的会员,看了一下还有几十个TB的空间还没用上.于是考虑把这个网盘变成一个定向共享数据的 ...

  7. 百度指数 Cipher-Text、百度翻译 Acs-Token 逆向分析

    K 哥之前写过一篇关于百度翻译逆向的文章,也在 bilibili 上出过相应的视频,最近在 K 哥爬虫交流群中有群友提出,百度翻译新增了一个请求头参数 Acs-Token,如果不携带该参数,直接按照以 ...

  8. 人工智能创新挑战赛:海洋气象预测Baseline[4]完整版(TensorFlow、torch版本)含数据转化、模型构建、MLP、TCNN+RNN、LSTM模型训练以及预测

    人工智能创新挑战赛:海洋气象预测Baseline[4]完整版(TensorFlow.torch版本)含数据转化.模型构建.MLP.TCNN+RNN.LSTM模型训练以及预测 1.赛题简介 项目链接以及 ...

  9. C/C++ 实现简易HTTP服务器

    #include <stdio.h> #include <stdlib.h> #include <process.h> #include <WinSock2. ...

  10. Tire树 学习笔记

    定义与基本求法 定义 又称字典树,用边表示字母,从根节点到树上某一节点路径形成一个字符串. 例如 \(charlie:\) 基本求法 廷显然的,往树中存就行了,查询也是显然的,通过一道例题来理解吧: ...