Kotlin 协程四 —— Flow 和 Channel 的应用
一、 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) {}
consumeAsFlow
和 receiveAsFlow
都是调用 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 的应用的更多相关文章
- Kotlin协程通信机制: Channel
Coroutines Channels Java中的多线程通信, 总会涉及到共享状态(shared mutable state)的读写, 有同步, 死锁等问题要处理. 协程中的Channel用于协程间 ...
- Kotlin 协程一 —— 全面了解 Kotlin 协程
一.协程的一些前置知识 1.1 进程和线程 1.1.1基本定义 1.1.2为什么要有线程 1.1.3 进程与线程的区别 1.2 协作式与抢占式 1.2.1 协作式 1.2.2 抢占式 1.3 协程 二 ...
- rxjava回调地狱-kotlin协程来帮忙
本文探讨的是在tomcat服务端接口编程中, 异步servlet场景下( 参考我另外一个文章),用rxjava来改造接口为全流程异步方式 好处不用说 tomcat的worker线程利用率大幅提高,接口 ...
- Kotlin协程解析系列(上):协程调度与挂起
vivo 互联网客户端团队- Ruan Wen 本文是Kotlin协程解析系列文章的开篇,主要介绍Kotlin协程的创建.协程调度与协程挂起相关的内容 一.协程引入 Kotlin 中引入 Corout ...
- python并发编程之gevent协程(四)
协程的含义就不再提,在py2和py3的早期版本中,python协程的主流实现方法是使用gevent模块.由于协程对于操作系统是无感知的,所以其切换需要程序员自己去完成. 系列文章 python并发编程 ...
- Kotlin协程第一个示例剖析及Kotlin线程使用技巧
Kotlin协程第一个示例剖析: 上一次https://www.cnblogs.com/webor2006/p/11712521.html已经对Kotlin中的协程有了理论化的了解了,这次则用代码来直 ...
- Retrofit使用Kotlin协程发送请求
Retrofit2.6开始增加了对Kotlin协程的支持,可以通过suspend函数进行异步调用.本文简单介绍一下Retrofit中协程的使用 导入依赖 app的build文件中加入: impleme ...
- Kotlin协程基础
开发环境 IntelliJ IDEA 2021.2.2 (Community Edition) Kotlin: 212-1.5.10-release-IJ5284.40 我们已经通过第一个例子学会了启 ...
- Android Kotlin协程入门
Android官方推荐使用协程来处理异步问题.以下是协程的特点: 轻量:单个线程上可运行多个协程.协程支持挂起,不会使正在运行协程的线程阻塞.挂起比阻塞节省内存,且支持多个并行操作. 内存泄漏更少:使 ...
- Kotlin Coroutine(协程): 四、+ Retrofit
@ 目录 前言 一.准备工作 二.开始使用 1.简单使用 2.DSL 3.扩展函数 4.请求发起 总结 前言 Retrofit 从 2.6.0 版本开始, 内置了对 Kotlin Coroutines ...
随机推荐
- [转帖]Springboot容器化镜像设置堆内存大小
参考资料:Best Practices: Java Memory Arguments for Containers - DZone Java 有三种方式设置最大堆内存大小: 1. -Xmx 2. -X ...
- OpenEuler2203使用rpm方式安装Oracle19c的过程
OpenEuler2203使用rpm方式安装Oracle19c的过程 安装介质 oracle-database-preinstall-19c-1.0-1.el7.x86_64.rpm oracle-d ...
- arch linux 安装
好长时间都没有更新自己的博客了,我简单翻阅了一下自己的更新记录,上一次更新好像还是在5月份左右,距今也有快半年,这半年也是遇到了很多事情,有不好的,也有好的.这半年我对在日常生活工作中使用Linux系 ...
- 解锁数据潜力:信息抽取、数据增强与UIE的完美融合
解锁数据潜力:信息抽取.数据增强与UIE的完美融合 1.信息抽取(Information Extraction) 1.1 IE简介 信息抽取是 NLP 任务中非常常见的一种任务,其目的在于从一段自然文 ...
- 中文LLaMA模型和指令精调的Alpaca大模型:中文数据进行二次预训练
中文LLaMA模型和指令精调的Alpaca大模型:中文数据进行二次预训练,进一步提升了中文基础语义理解能力 以ChatGPT.GPT-4等为代表的大语言模型(Large Language Model, ...
- Prompt工程师指南[高阶篇]:对抗性Prompting、主动prompt、ReAct、GraphPrompts、Multimodal CoT Prompting等
Prompt工程师指南[高阶篇]:对抗性Prompting.主动prompt.ReAct.GraphPrompts.Multimodal CoT Prompting等 1.对抗性 Prompting ...
- 3.5 Windows驱动开发:应用层与内核层内存映射
在上一篇博文<内核通过PEB得到进程参数>中我们通过使用KeStackAttachProcess附加进程的方式得到了该进程的PEB结构信息,本篇文章同样需要使用进程附加功能,但这次我们将实 ...
- unordered_map模拟实现|STL源码剖析系列|开散列
博主很久没有更新过STL源码剖析这个系列的文章了,主要是因为大部分STL常用的容器,博主都已经发过文章了,今天博主带着大家把哈希表也模拟实现一下. 前言 那么这里博主先安利一下一些干货满满的专栏啦! ...
- 手撕Udp套接字|实现群聊通信|实现Windows & Linux通信交互
专栏和Git地址 操作系统https://blog.csdn.net/yu_cblog/category_12165502.html?spm=1001.2014.3001.5482UdpSocke ...
- 【奶奶看了也不会】AI绘画 Mac安装stable-diffusion-webui绘制AI妹子保姆级教程
1.作品图 2.准备工作 目前网上能搜到的stable-diffusion-webui的安装教程都是Window和Mac M1芯片的,而对于因特尔芯片的文章少之又少,这就导致我们还在用老Intel 芯 ...