一、 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. [转帖]Springboot容器化镜像设置堆内存大小

    参考资料:Best Practices: Java Memory Arguments for Containers - DZone Java 有三种方式设置最大堆内存大小: 1. -Xmx 2. -X ...

  2. OpenEuler2203使用rpm方式安装Oracle19c的过程

    OpenEuler2203使用rpm方式安装Oracle19c的过程 安装介质 oracle-database-preinstall-19c-1.0-1.el7.x86_64.rpm oracle-d ...

  3. arch linux 安装

    好长时间都没有更新自己的博客了,我简单翻阅了一下自己的更新记录,上一次更新好像还是在5月份左右,距今也有快半年,这半年也是遇到了很多事情,有不好的,也有好的.这半年我对在日常生活工作中使用Linux系 ...

  4. 解锁数据潜力:信息抽取、数据增强与UIE的完美融合

    解锁数据潜力:信息抽取.数据增强与UIE的完美融合 1.信息抽取(Information Extraction) 1.1 IE简介 信息抽取是 NLP 任务中非常常见的一种任务,其目的在于从一段自然文 ...

  5. 中文LLaMA模型和指令精调的Alpaca大模型:中文数据进行二次预训练

    中文LLaMA模型和指令精调的Alpaca大模型:中文数据进行二次预训练,进一步提升了中文基础语义理解能力 以ChatGPT.GPT-4等为代表的大语言模型(Large Language Model, ...

  6. Prompt工程师指南[高阶篇]:对抗性Prompting、主动prompt、ReAct、GraphPrompts、Multimodal CoT Prompting等

    Prompt工程师指南[高阶篇]:对抗性Prompting.主动prompt.ReAct.GraphPrompts.Multimodal CoT Prompting等 1.对抗性 Prompting ...

  7. 3.5 Windows驱动开发:应用层与内核层内存映射

    在上一篇博文<内核通过PEB得到进程参数>中我们通过使用KeStackAttachProcess附加进程的方式得到了该进程的PEB结构信息,本篇文章同样需要使用进程附加功能,但这次我们将实 ...

  8. unordered_map模拟实现|STL源码剖析系列|开散列

    博主很久没有更新过STL源码剖析这个系列的文章了,主要是因为大部分STL常用的容器,博主都已经发过文章了,今天博主带着大家把哈希表也模拟实现一下. 前言 那么这里博主先安利一下一些干货满满的专栏啦! ...

  9. 手撕Udp套接字|实现群聊通信|实现Windows & Linux通信交互

    ​ 专栏和Git地址 操作系统https://blog.csdn.net/yu_cblog/category_12165502.html?spm=1001.2014.3001.5482UdpSocke ...

  10. 【奶奶看了也不会】AI绘画 Mac安装stable-diffusion-webui绘制AI妹子保姆级教程

    1.作品图 2.准备工作 目前网上能搜到的stable-diffusion-webui的安装教程都是Window和Mac M1芯片的,而对于因特尔芯片的文章少之又少,这就导致我们还在用老Intel 芯 ...